0) 세션이 왜 필요했을까? (세션이 없으면 생기는 문제)
세션(또는 토큰) 같은 “로그인 상태 유지 장치”가 없다면, 서버는 요청을 받을 때마다 사용자가 누군지 알 방법이 없습니다.
그러면 로그인 이후에도 다음 같은 일이 생겨요:
- 사용자가 장바구니 담기 버튼을 누름 → 서버는 “누가 눌렀는지” 몰라서 다시 로그인 요구
- 마이페이지, 주문내역, 좋아요 같은 “내 정보”가 필요한 페이지를 누를 때마다 로그인 요구
- 심지어 “로그인 성공” 직후에 페이지 이동(
GET /me)만 해도, 서버는 다시 “누구세요?”가 됨
즉, “로그인 한 번으로 여러 요청에서 동일 사용자를 이어서 식별”하려면
브라우저가 매 요청마다 뭔가를 같이 보내줘야 하고(쿠키/헤더), 서버도 그걸 해석할 규칙이 필요합니다.
1) Session 개념
세션 방식은 보통 이렇게 나뉩니다.
- 서버: “로그인 상태(사용자 정보/권한 등)”를 세션 저장소(메모리/Redis/DB)에 저장
- 클라이언트(브라우저): “세션을 가리키는 값”인 세션ID(Session ID) 를 쿠키로 저장하고, 매 요청마다 자동 전송
쿠키는 서버가 Set-Cookie로 내려주고, 브라우저는 이후 요청에 Cookie 헤더로 자동 포함합니다.
실제 값 예시 (Set-Cookie / Cookie)
서버가 로그인 성공 후 내려주는 응답 헤더 예시:
Set-Cookie: SESSIONID=9bb0422b85e7acb50d25d57147310672; Path=/; HttpOnly; Secure; SameSite=Lax이후 브라우저 요청 예시:
Cookie: SESSIONID=9bb0422b85e7acb50d25d57147310672서버는 세션ID로 세션 저장소를 조회해요. 예를 들어 Redis/DB에 이런 형태로 저장될 수 있습니다:
{
"sessionId": "9bb0422b85e7acb50d25d57147310672",
"userId": 123,
"role": "user",
"createdAt": "2026-01-05T09:12:33+09:00",
"lastSeenAt": "2026-01-05T09:25:10+09:00"
}2) Session를 통한 로그인 방법
아래는 세션 로그인 전체 흐름입니다.
sequenceDiagram autonumber participant C as Client participant S as Server participant SS as SessionStore C->>S: POST /login S->>S: Verify credentials S->>SS: Create sessionId and save userId role SS-->>S: Saved S-->>C: Set-Cookie SESSIONID=sessionId C->>S: Request with Cookie SESSIONID S->>SS: Lookup session by sessionId SS-->>S: session data userId role S-->>C: Authorized response
순서별 설명
- Client가 로그인 요청을 보냅니다.
- Server가 아이디/비밀번호를 검증합니다.
- Server가
sessionId를 만들고, 사용자 정보(userId,role)를 세션 저장소에 저장합니다. - SessionStore가 저장 완료를 응답합니다.
- Server가
Set-Cookie: SESSIONID=sessionId로 세션ID를 내려줍니다. - 이후 Client는 요청마다
Cookie: SESSIONID=...를 자동으로 포함해 보냅니다. - Server는 쿠키의
SESSIONID로 세션 저장소에서 세션을 조회합니다. - SessionStore가 세션 데이터(예:
userId,role)를 반환합니다. - Server는 조회 결과를 바탕으로 인가 후 응답합니다.
“양념 예시”: 주문조회 API를 호출할 때 실제로는?
- 요청:
GET /orders+Cookie: SESSIONID=9bb0422b85e7acb50d25d57147310672 - 서버:
SESSIONID=9bb0422b85e7acb50d25d57147310672로 세션 조회 →userId=123확인 - 그
userId=123의 주문만 조회해서 응답
3) Session 방법의 한계
3.1 확장성(Scale-out) 부담
서버가 여러 대면, 세션이 어느 서버/어느 저장소에 있느냐가 문제가 됩니다.
그래서 sticky session 또는 중앙 세션 저장소(예: Redis)가 필요해지곤 합니다.
3.2 세션ID 보호가 곧 보안
세션 방식에서 진짜 “열쇠”는 세션ID입니다.
SESSIONID가 탈취되면, 공격자는 그 세션으로 로그인된 사용자처럼 행동할 수 있어요.
그래서 쿠키는 보통 아래 옵션을 고려합니다(브라우저 보안 속성):
HttpOnly: JS에서 쿠키 접근 제한(일부 XSS 피해 완화)Secure: HTTPS에서만 전송SameSite: 크로스 사이트 자동 전송 제한(일부 CSRF 완화)
“양념 예시”: 세션 방식 로그아웃이 쉬운 이유
세션 방식 로그아웃은 보통 이렇게 끝납니다.
- 서버가
9bb0422b85e7acb50d25d57147310672세션 레코드를 삭제 - 브라우저 쿠키도 만료(또는 삭제) 처리
→ “서버가 상태를 들고 있기에” 강제 로그아웃/차단이 쉽습니다.
4) JWT 등장, 개념
JWT는 클레임(Claim) 을 JSON으로 담아, 토큰 하나로 들고 다니는 형식입니다.
JWT 문자열은 보통 이렇게 생겼습니다:
header.payload.signature
header: 토큰 타입, 서명 알고리즘 등 메타 정보payload: Claim (예: sub, role, iat, exp …)signature:base64url(header) + "." + base64url(payload)를 비밀키로 서명한 값(무결성 검증)
중요한 포인트:
payload는 “암호화”가 아니라 Base64URL 인코딩입니다.
즉 누구나 디코딩해서 내용을 볼 수 있어요 → 민감정보를 넣으면 안 됩니다.
4.1 실제 JWT 예시 (진짜처럼 생긴 값)
아래는 학습용으로 만든 HS256(HMAC-SHA256) 서명 JWT 예시입니다.
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9.7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM이 토큰을 . 기준으로 나누면:
- header(1번째 조각) =
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 - payload(2번째 조각) =
eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9 - signature(3번째 조각) =
7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM
4.2 header 디코딩 예시
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 를 Base64URL 디코딩하면:
{
"alg": "HS256",
"typ": "JWT"
}여기서 핵심은:
typ: JWTalg: HS256 (대칭키 HMAC-SHA256)
4.3 payload(Claim) 디코딩 예시
eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9 를 Base64URL 디코딩하면 Claim이 나옵니다:
{
"sub": "u123",
"role": "admin",
"iat": 1700000000,
"exp": 1700000600
}예시 Claim 의미:
sub: 사용자 식별자(Subject) →"u123"role: 권한 →"admin"iat: 발급 시각exp: 만료 시각(이 시간이 지나면 거부)
5) JWT를 통한 로그인 방법
JWT 기반 로그인/인가 흐름은 보통 아래처럼 갑니다.
sequenceDiagram autonumber participant C as Client participant A as AuthServer participant R as ResourceServer C->>A: POST /login A->>A: Verify credentials A-->>C: Issue access token JWT C->>R: Request with Authorization Bearer JWT R->>R: Verify signature and claims exp R-->>C: Authorized response
순서별 설명
- Client가 로그인 요청을 보냅니다.
- AuthServer가 아이디/비밀번호를 검증합니다.
- 검증이 성공하면 AuthServer가 Access Token(JWT) 을 발급해서 내려줍니다.
- 이후 Client는 API 호출마다
Authorization: Bearer <JWT>로 토큰을 전송합니다. - ResourceServer는 토큰의 서명(signature) 과 클레임(exp 등) 을 검증합니다.
- 검증이 통과되면 인가 후 응답합니다.
“양념 예시”: 실제 요청 헤더는 이렇게 보임
GET /me HTTP/1.1
Host: api.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1MTIzIiwicm9sZSI6ImFkbWluIiwiaWF0IjoxNzAwMDAwMDAwLCJleHAiOjE3MDAwMDA2MDB9.7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM6) JWT 서명 검증을 “실제 값”으로 이해하기
서버는 내부적으로 이런 검증을 합니다(개념):
- 클라이언트가 보낸 토큰에서
header와payload를 그대로 떼어냄 - 서버가 가진 비밀키(secret) 로
signature를 다시 계산 - 계산 결과가 토큰의
signature와 같으면 “변조 없음”으로 판단 - 그리고
exp(만료), 필요하면iss/aud같은 claim도 추가로 검증
“양념 예시”: payload만 바꿔치기하면?
공격자가 payload에서 role을 바꾸고(signature는 그대로 둠):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ1MTIzIiwicm9sZSI6InN1cGVyYWRtaW4iLCJpYXQiOjE3MDAwMDAwMDAsImV4cCI6MTcwMDAwMDYwMH0.7mSkACWZjbhKP6qNOLLMBayTVLCidxQfH_YBiGvkjIM- 토큰 자체는 “그럴듯”하게 보이지만
- 서버가 signature를 재계산하면 불일치 → 거부되어야 정상입니다.
7) JWT 방법의 한계
7.1 강제 로그아웃/폐기가 세션보다 어렵다
세션은 서버가 지우면 끝인데, JWT는 클라이언트가 들고 있으면 exp 만료 전까지 유효할 수 있습니다.
즉 “로그아웃했는데, 토큰이 유출된 공격자는 만료 전까지 호출” 같은 문제가 생길 수 있어요.
7.2 Bearer 토큰 특성
Bearer 토큰은 “가진 사람이 주인”처럼 동작하기 쉬워서 유출 시 위험합니다.
그래서 HTTPS 강제, 저장소 선택(쿠키/스토리지), 만료 짧게 등 설계가 중요해집니다.
8) JWT 보완: Access Token / Refresh Token
실무에서는 보통 2-토큰 패턴을 씁니다.
- Access Token: 짧게(예: 5~15분) → API 호출용
- Refresh Token: 길게(예: 며칠~몇 주) → Access Token 재발급용
아래는 흐름입니다.
sequenceDiagram autonumber participant C as Client participant A as AuthServer participant R as ResourceServer C->>A: Login success A-->>C: access token short A-->>C: refresh token long C->>R: API call with access token R->>R: Verify access token R-->>C: Response C->>R: API call with expired access token R-->>C: 401 token expired C->>A: POST /token with refresh token A->>A: Validate refresh token A-->>C: New access token
순서별 설명
- 로그인에 성공하면 AuthServer가
access token(짧은 수명)과refresh token(긴 수명)을 내려줍니다. - Client는 API 호출에
access token을 사용합니다. - ResourceServer는 access token을 검증하고 정상 응답합니다.
- 시간이 지나 access token이 만료되면, 동일 호출이 실패할 수 있습니다.
- 서버는 만료를 감지하고(예:
401) 재발급이 필요하다는 신호를 줍니다. - Client는
refresh token으로 토큰 엔드포인트(/token)에 재발급 요청을 보냅니다. - AuthServer가 refresh token을 검증합니다.
- 검증이 통과되면 새 access token을 발급해서 내려줍니다.
“양념 예시”: refresh token은 보통 의미 없는 랜덤 문자열(opaque)
예시:
rt_7FjQLL58aHUUMlKj-GcxbmO1ENz6rshgp8JDg11XbMg그리고 서버는 refresh token을 DB/Redis에 저장하고(폐기/회전/재사용 감지),
클라이언트가 access token 만료 시에만 재발급에 사용하게 만듭니다.
9) 참고 자료
- RFC 6265: HTTP State Management Mechanism (Cookies)
- MDN: Cookies 보안 옵션(Secure/HttpOnly/SameSite)
- OWASP: Session Management Cheat Sheet
- RFC 7519: JSON Web Token (JWT)
- RFC 6750: Bearer Token Usage (Authorization: Bearer)
- RFC 8725: JWT Best Current Practices
- RFC 6749: OAuth 2.0 (access/refresh token 개념)
- RFC 9700: OAuth 2.0 Security Best Current Practice