1. JWT의 정의와 특징

JWT는 당사자 간에 정보를 JSON 객체로 안전하게 전송하기 위한 표준이다. 웹에서 인증과 권한 부여를 구현할 때 가장 널리 사용되는 수단 중 하나이다. 토큰 자체가 사용자의 권한 정보나 서비스 상태를 포함하고 있어, 이를 수신하는 서버는 별도의 데이터베이스 조회 없이도 토큰 내의 정보만으로 요청을 처리할 수 있다.




2. JWT의 구조

JWT는 마침표(.)를 구분자로 하여 세 가지 부분으로 나뉜다. 각 부분은 Base64Url 방식으로 인코딩되어 하나의 문자열을 형성한다.


2-1. Header

헤더는 토큰에 대한 메타데이터를 담고 있다. 일반적으로 두 가지 정보를 포함한다.


{
    "alg": "HS256",
    "typ": "JWT"
}

2-2. Payload (페이로드)

페이로드에는 전달하고자 하는 실제 정보인 Claim 이 포함된다.

{
    "name": "Hong",
    "email": "Hong@example.com",
}

Claim 은 크게 세 가지 종류로 구분


2-3. Signature

Signature 는 토큰의 무결성을 증명하는 핵심 부분이다. 인코딩된 헤더와 페이로드를 합친 뒤, 서버가 안전하게 보관 중인 Secret Key 를 사용하여 헤더에 명시된 알고리즘으로 해싱한다. 이를 통해 서버는 전달받은 토큰이 위변조되지 않았음을 검증할 수 있다.




3. 인증 방식의 변화: 세션 기반 vs 토큰 기반

로그인 상태를 유지하는 방식은 서버의 상태 저장 여부에 따라 크게 두 가지로 나뉜다.


3-1. Stateful → 세션/쿠키

전통적인 세션 방식은 서버가 사용자의 상태를 메모리나 데이터베이스에 유지한다.


3-2. Stateless → JWT

JWT는 서버가 상태를 저장하지 않는 Stateless 아키텍처를 지향한다.

구분 세션/쿠키 방식 JWT 방식
저장 위치 서버(Memory/DB) 및 클라이언트 쿠키 클라이언트 (Local Storage/Cookie)
확장성 낮음 (세션 동기화 필요) 높음 (어느 서버에서나 검증 가능)
보안성 세션 ID 탈취 시 위험하지만 서버 제어 가능 토큰 탈취 시 만료 전까지 무방비 상태


4. 보안 강화를 위한 Access & Refresh Token

이를 위해 Access TokenRefresh Token 을 병행하는 전략이 사용된다. 이 방식을 통해 사용자는 빈번한 로그인 없이 서비스를 이용할 수 있으며, 서버는 Refresh Token 을 검증하거나 차단함으로써 사용자의 접속을 제어할 수 있는 수단을 갖게 된다. 최근에는 보안성을 더 높이기 위해 Refresh Token 을 한 번 사용하면 폐기하고 재발급하는 방법도 사용하고 있다.




5. 안전한 토큰 보관소?

구분 Local Storage HttpOnly Cookie
주요 위협 XSS 취약 CSRF 취약
접근성 JS로 접근 👍 JS로 접근 👎 (HttpOnly 설정 시)
난이도 구현 간단 구현 복잡
결론 보안 수준 낮음 보안 수준 더 높음



6. Spring Security 와 JWT

Spring Security 상에서 JWT를 사용할 때 내부적으로 어떤 일이 일어나는지 공부해보자.


6-1. Spring 내부 전개

# == 전체 흐름 ==
사용자 요청
① Tomcat (웹 서버)
② DispatcherServlet (Spring MVC 입구)
③ Spring Security Filter Chain (여기서 JWT 검증!)
④ Controller (우리가 만든 코드)

① Tomcat이 요청을

사용자가 보낸 HTTP 요청:
┌─────────────────────────────────────┐
│ POST /api/posts HTTP/1.1            │
│ Authorization: Bearer eyJhbGc...    │  ← JWT 토큰!
│                                     │
{ "title": "안녕하세요" }└─────────────────────────────────────┘

② DispatcherServlet으로 전달


③ Spring Security Filter Chain


6-2. 성공 / 실패 케이스

인증 성공 케이스


[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에 도달 못함!

결국 핵심은

  1. 필터에서 JWT를 검증 하고
  2. SecurityContext에 인증 정보를 저장 하면
  3. Controller에서 자유롭게 사용자 정보를 쓸 수 있음!



7. 정리

JWT란?



Spring에서 JWT 사용 전개

// 로그인
POST /login → 토큰 생성 → "eyJhbG..." 반환

// 이후 요청  
GET /api/posts + Header(토큰) → Filter에서 검증 → Controller

Spring에 JWT 도입 시 만들어야 하는 것




8. Reference