HTTP 인증과 쿠키 심화 - 세션, JWT, 보안 속성
한 줄 요약: HTTP 인증은 Authorization 헤더로 자격 증명을 전달하고, 쿠키의 HttpOnly·Secure·SameSite 속성으로 세션 탈취 공격을 방어한다.
비유로 이해하는 인증과 세션
놀이공원 입장 시스템을 생각해보자.
- 최초 입장 = 로그인 (티켓 구매 → 팔찌 수령)
- 팔찌 = 세션 쿠키 (가지고 있으면 어디든 입장 가능)
- 팔찌 확인 = 서버의 세션 검증
- 팔찌 분실 = 세션 만료 또는 탈취
팔찌(쿠키)만 있으면 누구든 입장할 수 있다. 그래서 팔찌가 타인에게 넘어가지 않도록 보호하는 것이 핵심이다.
인증(Authentication) vs 인가(Authorization)
| 구분 | 의미 | HTTP 헤더 | 실패 코드 |
|---|---|---|---|
| 인증(Authentication) | 당신이 누구인지 확인 | Authorization |
401 Unauthorized |
| 인가(Authorization) | 당신이 무엇을 할 수 있는지 확인 | (별도 없음) | 403 Forbidden |
sequenceDiagram
participant C as "클라이언트"
participant S as "서버"
Note over C,S: "인증 실패 (401)"
C->>S: "1. GET /mypage (로그인 안 함)"
S-->>C: "2. 401 Unauthorized\nWWW-Authenticate: Bearer realm='api'"
Note over C,S: "인가 실패 (403)"
C->>S: "3. GET /admin (일반 사용자로 로그인)"
S-->>C: "4. 403 Forbidden (로그인은 됐지만 권한 없음)"
Authorization 헤더 인증 방식
Basic 인증
사용자 ID와 비밀번호를 Base64로 인코딩해서 전송한다.
Authorization: Basic dXNlcjpwYXNzd29yZA==
# 디코딩: user:password
graph LR
C["클라이언트"] -->|"GET /protected"| S["서버"]
S -->|"401 + WWW-Auth"| C
C -->|"base64 인코딩"| C
C -->|"Authorization:Basic"| S
S -->|"200 OK"| C
주의: Base64는 암호화가 아니라 인코딩이다. 반드시 HTTPS와 함께 사용해야 한다.
Bearer 토큰 인증 (JWT)
현재 API 인증에서 가장 널리 사용되는 방식이다.
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwibmFtZSI6IuupleuqqeyeqiIsImV4cCI6MTcwMDAwMDAwMH0.signature
JWT 구조:
헤더.페이로드.서명
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 ← 헤더 (알고리즘, 타입)
.
eyJzdWIiOiIxMjM0IiwibmFtZSI6IuupleuqqeyeqiIsImV4cCI6MTcwMDAwMDAwMH0 ← 페이로드 (사용자 정보)
.
signature ← 서명 (위변조 검증)
세션 방식 vs JWT 방식:
| 항목 | 세션 방식 | JWT 방식 |
|---|---|---|
| 상태 저장 | 서버(DB/Redis)에 저장 | 클라이언트에 저장 |
| 확장성 | 서버 증설 시 세션 공유 필요 | 서버 무상태(Stateless) |
| 보안 | 서버에서 즉시 무효화 가능 | 만료 전 무효화 어려움 |
| 크기 | sessionId만 전송 (작음) | 토큰 자체가 큼 |
쿠키 상세 — Stateless 극복
쿠키 미사용 시 문제
graph LR
U["사용자"] -->|"POST /login"| S["서버(Stateless)"]
S -->|"200 OK"| U
U -->|"GET /welcome"| S
S -->|"안녕하세요 손님!(기억 못함)"| U
쿠키 사용 시 해결
graph LR
U["브라우저"] -->|"POST /login"| S["서버"]
S -->|"Set-Cookie: sid"| U
U -->|"GET /welcome+Cook"| S
S -->|"안녕하세요 홍길동님!"| U
U -->|"GET /orders+Cookie"| S
S -->|"주문 목록"| U
쿠키 생명주기 상세
세션 쿠키
Set-Cookie: tempPref=dark
Expires 또는 Max-Age를 지정하지 않으면 세션 쿠키가 된다. 브라우저 탭을 닫거나 브라우저를 종료하면 삭제된다.
영속 쿠키
# Expires: 절대 만료 날짜 지정
Set-Cookie: userId=hong; Expires=Thu, 01 Jan 2026 00:00:00 GMT
# Max-Age: 상대적 유효 시간 (초 단위, Expires보다 우선)
Set-Cookie: userId=hong; Max-Age=604800 # 7일
Set-Cookie: userId=hong; Max-Age=0 # 즉시 삭제
쿠키 보안 속성 심화
HttpOnly — XSS 방어
Set-Cookie: sessionId=abc123; HttpOnly
// HttpOnly 없을 때: 스크립트로 쿠키 접근 가능
document.cookie // "sessionId=abc123" ← 탈취 위험
// HttpOnly 있을 때
document.cookie // "" ← 접근 불가
XSS 공격자가 악성 스크립트를 삽입해도 쿠키를 읽을 수 없다.
Secure — HTTPS 전용
Set-Cookie: sessionId=abc123; Secure
- HTTPS 연결에서만 쿠키를 전송한다
- HTTP 평문 전송 시 쿠키가 포함되지 않아 도청 방지
SameSite — CSRF 방어
CSRF(Cross-Site Request Forgery)는 다른 사이트에서 사용자 브라우저를 이용해 요청을 위조하는 공격이다.
graph LR
U["사용자"] -->|"로그인"| B["bank.com"]
U -->|"evil.com 방문"| E["악성 사이트"]
E -->|"위조 요청 유도"| U
U -->|"Cookie 자동 포함"| B
B -->|"정상 처리 → 이체 실행!"| X["피해"]
SameSite로 방어:
# Strict: 외부 사이트에서 오는 요청에 쿠키 미포함
Set-Cookie: sessionId=abc; SameSite=Strict
# Lax: 안전한 메서드(GET) + 최상위 탐색에만 전송 (현재 기본값)
Set-Cookie: sessionId=abc; SameSite=Lax
# None: 항상 전송 (Secure 필수)
Set-Cookie: sessionId=abc; SameSite=None; Secure
| SameSite 값 | 외부 사이트 링크 클릭 | 외부 사이트 이미지 요청 | 외부 사이트 폼 POST |
|---|---|---|---|
| Strict | X | X | X |
| Lax | O | X | X |
| None | O | O | O |
실무 보안 쿠키 설정
Set-Cookie: sessionId=abc123;
Expires=Thu, 01 Jan 2026 00:00:00 GMT;
Path=/;
Domain=.example.com;
Secure;
HttpOnly;
SameSite=Lax
Spring Boot에서의 쿠키 보안 설정
// application.yml
server:
servlet:
session:
cookie:
http-only: true
secure: true
same-site: lax
max-age: 3600 # 1시간
// 직접 쿠키 생성 시
@PostMapping("/login")
public ResponseEntity<Void> login(@RequestBody LoginDto dto,
HttpServletResponse response) {
String sessionId = authService.login(dto);
Cookie cookie = new Cookie("sessionId", sessionId);
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
cookie.setMaxAge(3600);
response.addCookie(cookie);
return ResponseEntity.ok().build();
}
로그아웃 처리
sequenceDiagram
participant C as "클라이언트"
participant S as "서버"
participant D as "세션 저장소"
C->>S: "1. POST /logout\nCookie: sessionId=abc123"
S->>D: "2. sessionId=abc123 세션 삭제"
S-->>C: "3. 200 OK\nSet-Cookie: sessionId=; Max-Age=0; HttpOnly"
Note over C: "4. 쿠키 즉시 삭제"
웹 스토리지 vs 쿠키
쿠키 외에 브라우저가 데이터를 저장하는 방법이 두 가지 더 있다.
| 항목 | 쿠키 | localStorage | sessionStorage |
|---|---|---|---|
| 서버 전송 | 자동 전송 | 전송 안 함 | 전송 안 함 |
| 만료 | Expires/Max-Age | 영구 (수동 삭제) | 탭 닫으면 삭제 |
| 크기 | 4KB | 5~10MB | 5~10MB |
| 보안 | HttpOnly로 JS 접근 차단 가능 | JS로 항상 접근 가능 | JS로 항상 접근 가능 |
| 용도 | 인증, 세션 | 사용자 설정, 캐시 | 임시 상태 |
핵심 포인트 정리
- 인증(401) 은 “로그인 해주세요”, 인가(403) 는 “권한이 없습니다”다
- Basic 인증은 HTTPS 없이 사용하면 위험하다. ID/PW가 Base64로 노출된다
- JWT(Bearer)는 서버가 상태를 저장하지 않아 수평 확장에 유리하다
- HttpOnly는 XSS 공격으로부터, SameSite는 CSRF 공격으로부터 쿠키를 보호한다
- Secure 속성은 HTTPS에서만 쿠키가 전송되도록 강제한다
- 실무에서 세션 쿠키에는 반드시 HttpOnly + Secure + SameSite=Lax 를 적용한다
댓글