서버가 사용자 상태를 기억하고, 클라이언트에는 그것을 가리키는 식별자만 주는 방식.
흐름
- ① 로그인 성공
- ② 서버: 세션 저장소에 { "abc123" → {userId: 1, role: USER} } 저장
- ③ 응답: Set-Cookie: JSESSIONID=abc123; HttpOnly; Secure; SameSite=Lax
- ④ 이후 요청마다 브라우저가 자동으로 쿠키 전송
- ⑤ 서버: abc123으로 저장소 조회 → 사용자 확인
클라이언트는 "abc123"이라는 무의미한 문자열만 갖는다. 실제 정보는 전부 서버에 있다. 이것이 JWT와의 근본 차이다.
장점
| 장점 | 설명 |
|---|---|
| 즉시 무효화 | 저장소에서 지우면 그 순간 로그아웃. 강제 로그아웃·계정 정지에 필수 |
| 정보 노출 없음 | 클라이언트가 가진 것은 무의미한 ID |
| 크기가 작다 | 매 요청에 수십 바이트만 |
| 상태 변경 즉시 반영 | 권한을 바꾸면 다음 요청부터 적용 |
단점 — 확장
-
서버가 여러 대면 세션을 어디에 둘 것인가?
-
① 서버 메모리 — → 다른 서버로 요청이 가면 로그인이 풀린다
-
② 세션 고정(sticky) — → 그 서버가 죽으면 세션 소실. 부하도 불균등
-
③ 세션 복제 — → 서버가 늘수록 복제 비용 폭증
-
④ 외부 저장소(Redis) — → ✅ 실무 표준
// Spring Session — 코드 변경 없이 저장소만 교체된다
implementation 'org.springframework.session:spring-session-data-redis'
Redis가 단일 장애점이 되지 않도록 복제·클러스터 구성이 필요하다.
쿠키 보안 속성 — 반드시 알아야 한다
Set-Cookie: SID=abc123; HttpOnly; Secure; SameSite=Lax; Path=/; Max-Age=1800
| 속성 | 막는 것 |
|---|---|
| HttpOnly | JS(document.cookie)로 못 읽음 → XSS로 탈취 방지 |
| Secure | HTTPS에서만 전송 → 중간자 도청 방지 |
| SameSite | 다른 사이트발 요청에 안 붙음 → CSRF 완화 |
| Path / Domain | 전송 범위 제한 |
| Max-Age | 만료 시간 |
SameSite 값
- Strict : 외부 링크로 들어와도 안 붙음. 가장 안전하지만 불편
- Lax — : GET 최상위 이동에는 붙음 (현대 브라우저 기본값)
- None — : 항상 붙음. 반드시 Secure와 함께여야 함 (크로스 도메인 필요 시)
세션 고정 공격 — 흔히 놓치는 부분
- ① 공격자가 미리 세션 ID를 알아낸 뒤 피해자 브라우저에 심는다
- ② 피해자가 그 세션 ID로 로그인한다
- ③ 공격자는 같은 세션 ID를 갖고 있으므로 로그인된 상태를 공유하게 된다
대응 — 로그인 성공 시 반드시 세션 ID를 새로 발급한다.
http.sessionManagement(s -> s.sessionFixation().changeSessionId());
스프링 시큐리티는 기본으로 이렇게 동작한다.
JWT와의 선택
세션이 유리 : 즉시 무효화가 필요, 단일 도메인, 권한이 자주 바뀜
- JWT가 유리 — : 여러 서비스가 공유, 서버 조회 없이 검증, 모바일·외부 API
"확장 때문에 무조건 JWT"는 낡은 통념이다. Redis 세션 조회는 1ms도 안 걸리고, 그 대가로 즉시 무효화를 얻는다. 많은 서비스에서 세션이 더 나은 선택이다.