TL;DR
- 쿠키(Cookie)는 브라우저가 서버에 자동 전송하는 작은 key-value storage이며, 보안 플래그 설정이 핵심입니다.
- 세션(Session)은 서버 측 상태 저장(세션 ID만 클라이언트 보관)으로 무효화·강제 로그아웃이 용이합니다.
- JWT는 서명된 토큰으로 stateless 검증이 장점이나 폐기, 회수가 어렵습니다. (회피: 짧은 수명 + refresh token 회전)
- 브라우저 스토리지(localStorage/sessionStorage/IndexedDB)는 스크립트 접근 가능 → XSS에 취약하므로 민감 비밀 보관 금지가 원칙입니다.
- 권장 기본기: 짧은 수명의 Access Token + HttpOnly·Secure·SameSite 쿠키 기반, BFF 패턴 또는 서버 세션/Redis 세션으로 운용, CSRF/XSS를 별도로 방어합니다.
1. 배경: 인증·인가·상태 관리
- 인증(Authentication): 사용자의 신원을 확인합니다(로그인).
- 인가(Authorization): 인증된 주체가 어떤 리스소·행위를 허용받는지 결정합니다.
- 상태 관리(State Management): 본질적으로 무상태(Stateless)인 HTTP 위에서 "누가 이미 로그인했는가, 권한은 무엇인가"를 요청 간 이어붙이는 기법입니다.
대표 전략:
- 서버 세션(Stateful): 서버가 사용자 상태를 저장, 클라이언트는 세션 식별자만 가짐
- 토큰 기반(Stateless): 서버가 토큰을 발급하고 요청마다 토큰 자체를 검증(서명 확인)
2. 쿠키(Cookie)
개요와 동작
- 서버 → 클라이언트:
Set-Cooke 헤더로 쿠키 설정
- 클라이언트 → 서버: 동인 도메인/경로/보안 조건에 맞으면 자동 전송
Set-Cookie: SID=abc123; Path=/; Secure; HttpOnly; SameSite=Lax; Max-Age=1200
주요 속성
- Secure: HTTPS에서만 전송
- HttpOnly: JS
document.cookie로 접근 불가 → XSS로부터 쿠키 탈취 완화
- SameSite:
Lax(기본) / Strict / None(+Secure 필수)
- Lax: 동일 사이트 요청에는 전송. 타 사이트에서 온 "최상위 GET 탐색"에도 전송
- Strict: 동일 사이트 요청에만 전송
- None: 교차 사이트 요청에도 전송
- Domain/Path: 전송 범위 제한
- Expires/Max-Age: 만료 시점
보안 포인트
- CSRF: 쿠키는 자동 전송되므로 SameSite + CSRF 토큰 조합을 사용합니다.
- XSS: HttpOnly로 탈취 난이도 상승. 그러나 쿠키 값 자체가 비밀이면 XSS에 취약합니다(예: 쿠키 값을 JS로 읽지 못해도, 악성 요청이 자동 전송될 수 있음).
- 쿠키에 토큰 저장 시: 가급적 HttpOnly + Secure + SameSite=Lax/Strict로 설정하고, 도메인 최소화가 좋습니다.
3. 세션(Session)
개념
- 클라이언트는 오직 세션 ID(대개 쿠키로 전달, 예:
JSESSIONID)만 보유
- 서버는 세션 저장소(메모리, DB, Redis 등)에 사용자 상태(인증·권한·부가정보)를 보관
장단점
- 장점:
- 서버가 상태를 통제 → 강제 로그아웃(세션 무효화), 권한 변경 즉시 반영이 용이
- 토큰 유출 대비면에서 상대적으로 안전(서명·클레임 노출 없음)
- 단점:
- 확장성: 서버 클러스터에서는 세션 공유 저장소(예: Redis) 필요 or Stricky Session 의존
- 서버 메모리/저장소 사용량 증거
공격·대응
- 세션 고정(Session Fixation): 로그인 성공 시 세션 ID 재발급(migrate)으로 예방
- 세션 하이재킹: Secure/HttpOnly/SameSite, 짧은 타임아웃, IP/UA 바인딩, 2FA 등 병행
- 로그아웃 처리: 서버 저장소에서 세션 무효화 +
JSESSIONID 쿠키 삭제
예시 (Spring)
application.yml
server:
servlet:
session:
timeout: 30m
cookie:
http-only: true
secure: true
same-site: lax
SecurityFilterChain
@Bean
SecurityFilterChain security(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.enable())
.sessionManagement(sm -> sm
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
)
.logout(lo -> lo
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
);
return http.build();
}
Redis 세션 공유
@EnableRedisHttpSession
4. JWT(JSON Web Token)
구조
{header}.{payload}.{signature} (Base64URL 인코딩)
- Header: alg(서명 알고리즘), typ(JWT), kid(키 식별자)
- Payload(Claims):
iss(발급자), sub(주체), aud, exp, nbf, iaf, jti, scope/roles 등 민감정보 제외
- Signature: HMAC(대칭) 또는 RSA/ECDSA(비대칭)
장단점
- 장점:
- 무상태 검증 → 고성능/고확장(리소스 서버는 서명만 확인)
- 분산 마이크로서비스에서 중앙 세션 공유 없이 검증 가능
- 단점:
- 폐기·회수 어려움: 이미 발급된 토큰은 만료 전까지 유효
- 과다 클레임/큰 토큰은 헤더 크기 증가 → 네트워크 비용 증가
- 키 관리(회전·유출 대응) 필수
권장 패턴
- Access Token: 짧은 수명(분 단위)
- Refresh Token: 더 긴 수명(일~주), 서버 저장소/화이트리스트 + 회전(Rotation)
- 매 갱신 시 새 리프레시 발급, 이전 토큰 사용 시 재사용 탐지하고 세션 강제 종료
- Key Rotation:
kid와 JWKS로 주기적 키 교체
- 전달 위치:
Authorization: Bearer <access_token> 헤더 권장(쿠키 자동 전송에 의존하지 않음 → CSRF 표면 축소)
예시 (Spring Security)
@Bean
SecurityFilterChain api(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwkSetUri("https://idp.example.com/.well-known/jwks.json")
)
);
return http.build();
}
5. 브라우저 스토리지: localStorage·sessionStorage·IndexedDB
| 항목 | localStorage | sessionStorage | IndexedDB |
|---|
| 수명 | 반영구(삭제 시까지) | 탭/창 세션 동안 | 반영구(삭제 시까지) |
| 접근 | JS로 직접 접근 가능 | JS로 직접 접근 가능 | JS로 직접 접근 가능(비동기) |
| 용량 | 수 MB | 수 MB | 수십~수백 MB |
| 동기/비동기 | 동기 | 동기 | 비동기 |
| 보안 | XSS에 취약(비밀 보관 금지) | XSS에 취약 | XSS에 취약 |
| 용도 | 비민감 설정/캐시 | 탭 한정 임시 데이터 | 오프라인 캐시/대용량 데이터 |
원칙: 민감 비밀(Access/Refresh Token, 세션 키 등)을 브라우저 스토리지에 저장하지 않습니다.
6. CSRF, XSS와의 관계
- CSRF: “사용자 브라우저가 의도치 않게 인증된 요청을 보내는” 공격
- 해결:
SameSite=Lax/Strict + CSRF 토큰(동기화 토큰, Double Submit Cookie) + 쿠키 범위 최소화.
- Bearer 헤더 기반 API는 자동 전송이 없으므로 CSRF 표면이 상대적으로 작음.
- XSS: 신뢰되지 않은 스크립트가 DOM에서 실행됨
- 해결: CSP(Content Security Policy), 입력 검증/출력 인코딩, 템플릿 이스케이프, 라이브러리 최신화.
- 브라우저 스토리지/비HttpOnly 쿠키에 저장된 비밀은 XSS에 취약합니다.
Spring에서 쿠키 기반 폼 로그인이라면 CSRF 기본 활성 유지 권장. SPA라면 프록시/BFF 뒤에서 쿠키 + CSRF 토큰 헤더 전략이 안정적입니다.
7.SPA와 BFF(Backend For Frontend)
문제: SPA가 직접 API에 호출하고 토큰을 브라우저가 관리하면 XSS·토큰 유출 표면 증가.
해법(권장): BFF가 브라우저와 동일 출처로 동작하고, HttpOnly 쿠키를 통해 세션/토큰을 관리합니다.
- 브라우저 ↔ BFF(동일 도메인): 쿠키 기반(자동 전송)
- BFF ↔ 리소스 API: 내부 네트워크에서 Bearer 토큰 사용
- 이점: 브라우저는 토큰을 직접 보관하지 않음, CSRF·CORS 제어가 쉬움
8. OAuth 2.1 / OIDC 맥락의 권장 플로우
- Authorization Code + PKCE(공개 클라이언트, SPA/모바일)
- Access Token(짧게) + Refresh Token(길게, 회전)
- Refresh Token은 HttpOnly·Secure·SameSite 쿠키로 브라우저에 보관하거나, 아예 BFF 서버 세션에 보관
- 리소스 서버는 aud/iss/exp/nbf 검증, 스코프 기반 인가
9. 실전 설계 레시피
서버 렌더링 + 세션(전통적, 안정)
- 로그인 성공 → 서버 세션에 사용자 컨텍스트 저장, JSESSIONID 쿠키 발급
- Redis 세션 공유 + SameSite=Lax + Secure + HttpOnly
- CSRF 기본 활성, 로그인 직후 세션 ID 재발급
- 장점: 무효화 쉽고 구현 단순. 대규모 트래픽은 Redis 필요.
SPA + BFF(권장)
- BFF가 HttpOnly 쿠키로 사용자 세션/토큰 관리, 프론트는 Fetch 시 자격증명 포함
- CSRF 토큰 헤더(예:
X-CSRF-TOKEN) + SameSite
- 리소스 서버는 순수 Bearer 토큰 검증
- 장점: 프론트가 토큰 비밀을 직접 다루지 않음.
SPA 직접 토큰(불가피할 때만)
- Access는 메모리 저장, 페이지 리로드 시 재인증(또는 짧은 수명의 Refresh를 HttpOnly 쿠키로 보관하여 재발급 전용)
- localStorage/sessionStorage에 비밀 보관 금지
- CSP/XSS 방어를 매우 엄격히 적용