JSON Web TokenJWT는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 표준이다. 웹에서 인증과 권한 부여를 구현할 때 가장 널리 사용되는 수단 중 하나이다. 토큰 자체가 사용자의 권한 정보나 서비스 상태를 포함하고 있어, 이를 수신하는 서버는 별도의 데이터베이스 조회 없이도 토큰 내의 정보만으로 요청을 처리할 수 있다.
header.payload.signatureJWT는 마침표(.)를 구분자로 하여 세 가지 부분으로 나뉜다. 각 부분은 Base64Url 방식으로 인코딩되어 하나의 문자열을 형성한다.
헤더는 토큰에 대한 메타데이터를 담고 있다. 일반적으로 두 가지 정보를 포함한다.
{
"alg": "HS256",
"typ": "JWT"
}
alg: 서명 시 사용된 해싱 알고리즘 (예: HS256, RS256)typ: 토큰의 유형 (JWT)페이로드에는 전달하고자 하는 실제 정보인 Claim 이 포함된다.
{
"name": "Hong",
"email": "Hong@example.com",
}
Claim 은 크게 세 가지 종류로 구분
Claims
iss(발행자), exp(만료 시간), sub(제목), iat(발행 시간) 등Claims
Claims
Base64Url 로 단순 인코딩됨Signature 는 토큰의 무결성을 증명하는 핵심 부분이다. 인코딩된 헤더와 페이로드를 합친 뒤, 서버가 안전하게 보관 중인 Secret Key 를 사용하여 헤더에 명시된 알고리즘으로 해싱한다. 이를 통해 서버는 전달받은 토큰이 위변조되지 않았음을 검증할 수 있다.
로그인 상태를 유지하는 방식은 서버의 상태 저장 여부에 따라 크게 두 가지로 나뉜다.
세션/쿠키전통적인 세션 방식은 서버가 사용자의 상태를 메모리나 데이터베이스에 유지한다.
JWTJWT는 서버가 상태를 저장하지 않는 Stateless 아키텍처를 지향한다.
| 구분 | 세션/쿠키 방식 | JWT 방식 |
|---|---|---|
| 저장 위치 | 서버(Memory/DB) 및 클라이언트 쿠키 | 클라이언트 (Local Storage/Cookie) |
| 확장성 | 낮음 (세션 동기화 필요) | 높음 (어느 서버에서나 검증 가능) |
| 보안성 | 세션 ID 탈취 시 위험하지만 서버 제어 가능 | 토큰 탈취 시 만료 전까지 무방비 상태 |
탈취 시 제어 불가능 문제를 해결할 수 있을까?이를 위해 Access Token 과 Refresh Token 을 병행하는 전략이 사용된다. 이 방식을 통해 사용자는 빈번한 로그인 없이 서비스를 이용할 수 있으며, 서버는 Refresh Token 을 검증하거나 차단함으로써 사용자의 접속을 제어할 수 있는 수단을 갖게 된다. 최근에는 보안성을 더 높이기 위해 Refresh Token 을 한 번 사용하면 폐기하고 재발급하는 방법도 사용하고 있다.
Local Storage vs Cookie ?| 구분 | Local Storage | HttpOnly Cookie |
|---|---|---|
| 주요 위협 | XSS 취약 | CSRF 취약 |
| 접근성 | JS로 접근 👍 | JS로 접근 👎 (HttpOnly 설정 시) |
| 난이도 | 구현 간단 | 구현 복잡 |
| 결론 | 보안 수준 낮음 | 보안 수준 더 높음 |
Spring Security 상에서 JWT를 사용할 때 내부적으로 어떤 일이 일어나는지 공부해보자.
# == 전체 흐름 ==
사용자 요청
↓
① Tomcat (웹 서버)
↓
② DispatcherServlet (Spring MVC 입구)
↓
③ Spring Security Filter Chain (여기서 JWT 검증!)
↓
④ Controller (우리가 만든 코드)
사용자가 보낸 HTTP 요청:
┌─────────────────────────────────────┐
│ POST /api/posts HTTP/1.1 │
│ Authorization: Bearer eyJhbGc... │ ← JWT 토큰!
│ │
│ { "title": "안녕하세요" } │
└─────────────────────────────────────┘
어떤 Controller로 보내야 하지?URL이 /api/posts 니까... PostController로 보내야겠다!하지만 잠깐! 보내기 전에 Filter들을 먼저 통과시켜야 해![Filter 1: CORS 필터][Filter 2: JWT 인증 필터][Filter 3: 권한 체크 필터]
[0ms] 사용자 요청 도착
POST /api/posts
Authorization: Bearer eyJhbG...
[1ms] Tomcat이 받음
→ DispatcherServlet으로 전달
[2ms] DispatcherServlet
"Controller 찾기 전에 Filter 먼저!"
[3ms] CORS 필터 통과
"다른 도메인 요청이네? 허용할까? OK!"
[4ms] JWT 필터
├─ [4.1ms] 헤더에서 토큰 추출
├─ [4.2ms] 서명 검증 (암호화 연산)
├─ [4.3ms] 만료시간 확인
├─ [4.4ms] Payload에서 userId, role 추출
└─ [4.5ms] SecurityContext에 저장
[5ms] 권한 체크 필터
"ROLE_USER 권한 있네? 통과!"
[6ms] Controller 도착!
PostController.createPost() 실행
[10ms] 응답 반환
{
"id": 1, "title": "안녕하세요"
}
# == 토큰이 없는 경우 ==
요청: POST /api/posts
헤더: Authorization 없음
[JWT 필터]
→ "토큰이 없네? 그냥 통과" (인증 안 된 상태로)
[권한 체크 필터]
→ "이 API는 로그인 필요한데 인증 안 됐네?"
→ 401 Unauthorized 응답
Controller에 도달 못함!
# == 토큰이 만료된 경우 ==
요청: POST /api/posts
헤더: Authorization: Bearer (만료된토큰)
[JWT 필터]
→ 토큰 검증 중...
→ 만료시간 확인: 2024-02-08 14:00 < 현재 15:00
→ ExpiredJwtException 발생!
→ 401 응답: "토큰이 만료되었습니다"
Controller에 도달 못함!
# == 토큰이 위조된 경우 ==
요청: POST /api/posts
헤더: Authorization: Bearer (해커가조작한토큰)
[JWT 필터]
→ 서명 검증 중...
→ 계산된 서명 ≠ 토큰의 서명
→ JwtException 발생!
→ 401 응답: "유효하지 않은 토큰입니다"
Controller에 도달 못함!
JWT
Cookie
// 로그인
POST /login → 토큰 생성 → "eyJhbG..." 반환
// 이후 요청
GET /api/posts + Header(토큰) → Filter에서 검증 → Controller
JwtTokenProvider → 토큰 생성/검증JwtAuthenticationFilter → 요청마다 토큰 체크SecurityConfig → 필터 등록