# 개념 2. JWT 액세스 토큰의 생성과 전달, Stateful한 리프레시 토큰의 생성, 전달, 보존

[이전 글](https://blog.letsdev.me/jwt-authentication)에서 정리에 꽤 힘을 뺐기 때문에, 이번 글에서는 서두와 부연설명을 줄이고 필요한 정보를 담아 전달해 보겠습니다.

---

# JWT 액세스 토큰

JWT 액세스 토큰은 인가에 직접 사용되는 토큰이고, stateless 하다는 장점이 있었습니다.

## 액세스 토큰의 생성

JWT(JSON Web Token)로 생성합니다. 비밀번호 인증 등 자격 검토 후 JWT를 발급합니다.

JWT는 헤더, 페이로드, 시그니처 세 영역을 점(.) 기호로 구분한다고 했습니다.

* **헤더**: 이 토큰에 대한 메타 데이터입니다. 즉, 이 토큰이 무엇인지 설명하는 부연 정보입니다.
    
* **페이로드**: 전달하는 본문 메시지입니다. 페이로드 객체의 각 항목을 claim(클레임)이라고 합니다.
    
    우리가 JWT 인증·인가에서 주로 관심을 갖는 주요 클레임(claim)은 다음과 같습니다.
    
    * `sub` (subject): 전달하는 주제(주요 대상)입니다. 주로 사용자 계정 이름(username)이나 이메일 또는 매핑된 식별 값을 담습니다.
        
    * 역할(`role`) 또는 권한(`authority`): 커스텀 클레임(claim)입니다.
        
    * `exp` (expiration time): JWT를 만료시키는 시각입니다.
        
    
    표준 스펙에 등록된 클레임 목록(Registered Claims)은 다음과 같습니다. 이것을 모두 포함해야 하는 것은 아니며, 원하는 새 항목을 추가해도 됩니다.
    
    * `sub` (subject): 전달하는 주제입니다.
        
    * `iat` (issued at time): JWT를 발급한 시각입니다.
        
    * `exp` (expiration time): JWT를 만료시키는 시각입니다.
        
    * `iss` (issuer): JWT 발급자 정보입니다.
        
    * `aud` (audience): JWT를 수신할 대상입니다.
        
    * `nbf` (not before time): JWT가 아직 효력을 갖지 않는 시각을 `nbf`에 설정할 수 있습니다.
        
    * `jti` (JWT ID): JWT에 고유 ID를 할당하면, 이 JWT를 일회용으로 사용하는 시스템 등에 재사용 탐지와 방지에 활용하도록 제공할 수 있습니다. (JWT 전체 대신 `jti`를 보존)
        
* **시그니처**: 헤더와 페이로드가 위·변조된 것이 아니라는 것을 확인시켜 주는 값입니다. 이는 HMAC 알고리즘으로 헤더와 페이로드를 해싱해서 생성합니다.
    

헤더와 페이로드에 민감한 정보를 담으면 안 됩니다. Base64 인코딩은 암호화가 아니므로 누구나 제약 없이 디코딩을 할 수 있습니다.

## 액세스 토큰의 전달과 보존

일반적으로 액세스 토큰 보존 방식은 클라이언트에 결정을 맡길 수 있습니다. 즉, 백엔드 개발자가 직접 신경쓸 부분은 전달까지고, 이를 보존하는 것은 프론트엔드 개발자가 신경써야 합니다.

**액세스 토큰의 전달**

* HTTP 응답 바디의 `access_token` 필드에 담아서 반환합니다.
    
* HTTPS 환경에서 응답하여야 합니다.
    
* 서버에는 보존하지 않습니다. (서버에 보존하지 않기 위해 JWT로 생성했습니다.)
    
    특수한 목적이 있을 때는 보존할 수 있지만, 유출에 대비한 형태로 보존합니다.
    

**액세스 토큰의 보존** (주로 프론트엔드 작업)

* 클라이언트가 보존합니다.
    
* **많은 회사**: 체감상 많은 회사가 로컬 스토리지(local storage)에 JWT 액세스 토큰을 보존합니다.
    
* **많은 권고**: 상태 관리 라이브러리 등을 사용할 수 있습니다. 새로고침 시 값이 손실되므로, 리프레시 요청을 바로 보내어 약간의 지연 이후부터 API를 이용할 수 있게 됩니다.
    
* 해석
    
    * 보안을 더 중요시하려면, 누군가의 실수로 취약점이 발생하더라도 액세스 토큰 탈취 가능성이 줄어들도록 권고대로 private 변수에 보존하는 방식을 택할 수 있습니다.
        
    * 따로 취약점이 발생하지 않는다면, 로컬스토리지에 저장하는 방식이 개발 편의성도 높으면서, 페이지 진입 초기에 사용자에게 빠른 API를 제공합니다. (취약점은 언제든 생길 수 있기 때문에 완전히 안전한 조치는 아닙니다.)
        

---

# 리프레시 토큰

JWT 액세스 토큰의 stateless 인가는 ‘즉시 만료’가 안 되어 악의적 사용자를 즉시 걸러낼 수 없으며, 액세스 토큰의 수명 동안 계속 통과되기 때문에 이로 인한 잠재적인 위협을 내포할 수 있다고 했습니다. 따라서 우리는 액세스 토큰의 수명을 짧게 하고, 이를 리프레시 하는 과정에서 stateful 인가로 사용자의 자격 검토에 관여할 기회를 빈번하게 갖습니다. 리프레시 자격 증명 수단이 리프레시 토큰입니다.

## 리프레시 토큰의 생성

Stateful 인가를 위해 생성하는 토큰이기 때문에, JWT로 생성하지 않습니다.

* 암호학적으로 안전한 랜덤 함수로 생성합니다.  
    (자바는 `SecureRandom` 등이 안전하다고 평가받습니다.)
    
* 길이는 보안 강도에 따라 선택합니다.  
    잘 모르겠다면 128 bits(16 Bytes)나 256 bits(32 bytes) 중에서 선택할 수 있습니다.
    

## 리프레시 토큰의 전달

리프레시 토큰은 쿠키에 저장하는 것을 권합니다. 이 쿠키는 다음 옵션을 포함해야 합니다.

**HTTP Only 옵션**

리프레시 토큰은 클라이언트 스크립트에 영향을 받지 않게 하여 XSS(Cross Site Scripting) 공격에 면역을 갖게 합니다. `HttpOnly` 옵션을 추가한 쿠키에 담으면 이 쿠키를 브라우저만 받고, 프론트엔드의 소스 코드가 동작하는 환경에는 제공해 주지 않겠다는 약속을 브라우저에 보낼 수 있습니다. 브라우저는 프론트엔드 스크립트에서는 `HttpOnly` 옵션이 있는 쿠키에 접근할 수 없게 하고, 서버에 요청을 보낼 때 자동으로 이 쿠키를 붙여서 보냅니다.

**Secure 옵션**

리프레시 토큰은 통신 구간에서 은닉되어야 합니다. 통신 구간 암호화는 HTTPS 통신을 통해 수행하며, `Secure` 옵션을 추가한 쿠키에 담으면 정상적인 브라우저는 HTTPS 요청에서만 쿠키를 전달합니다.

만약 서버가 `Secure` 옵션을 붙인 쿠키를 HTTPS가 아닌 HTTP 환경에서 전달해도, 정상 브라우저는 이 쿠키를 저장하지 않고 무시합니다. (이미 암호화되지 않은 통신 구간에 노출된 리프레시 토큰입니다. 재요청 시에도 재사용이 발생하지 않도록 새로 생성하는 로직을 유지하세요.)

**SameSite 옵션**

브라우저는 `SameSite=Lax` 또는 `SameSite=Strict` 옵션이 있는 쿠키를 외부 도메인에 보내는 요청에 담지 않을 것을 약속합니다.

위 쿠키 옵션은 정상적인 네트워크와 브라우저에서 수행되는 약속입니다. 취약한 환경을 이용하는 것은 여전히 사용자에 의해 발생할 수 있는 취약점입니다.

## 리프레시 토큰의 보존 (서버)

* 데이터베이스에 보존합니다. (Redis 등 휘발성 데이터 관리에 편한 DB)
    
* 리프레시 토큰을 해싱하여, 원문 유출이 없도록 저장합니다.
    
* 해싱 시 암호화 강도는 비밀번호 암호화에 비해 낮추고 사용자 경험과 균형을 맞출 수 있습니다.
    

**데이터베이스 (Redis Recommended)**

**휘발성 데이터를 관리하기 좋은 Redis에 리프레시 토큰을 담을 수 있습니다.** DB 종류에는 영향을 받지 않지만 사용자 경험을 위해 조회 성능을 고려하는 것이 좋습니다.

**Redis 대체재**

Valkey 데이터베이스는 Redis 데이터베이스와 동일한 프로토콜에서, 동일한 기본 CRUD API 스펙을 제공합니다. Redis 라이선스 이슈에 관심이 있다면 Valkey 같은 대체재를 사용해 보는 것도 좋습니다. Valkey를 사용하더라도 Redis 관련 라이브러리를 사용할 수 있으며, 기본 CRUD 작업을 위해 새로운 라이브러리를 추가하지 않아도 됩니다. (스탠드얼론 이미지와 클러스터 이미지가 따로 존재합니다.)

**해싱**

리프레시 토큰은 서버에서 유출되더라도 원문을 알 수 없도록 해싱을 하여 저장합니다.

해싱 강도는 비밀번호에 비해 낮추고 사용자 경험과 균형을 맞출 수 있습니다. 리프레시 토큰이 완전히 랜덤으로 생성되며, 그 평균 수명이 대체로 짧고, 최대 수명까지 사용되는 리프레시 토큰도 적으며, 공격자의 오프라인 어택으로 1초에 수억 개씩 해싱을 시도해도 리프레시 토큰의 최대 수명까지 우연히 해독될 가능성이 매우 낮기 때문입니다.

---

# 요약 정리

## 액세스 토큰 정리

* **생성**
    
    * 비밀번호 등 인증이 완료되면 JWT를 발행합니다.
        
    * 페이로드에는 최소 정보만 담아야 하며, 암호화되지 않습니다.  
        (Base64 인코딩은 누구나 디코딩 할 수 있으며, 암호화가 아닙니다.)
        
* **전달**: HTTPS 통신 환경에서 response body의 `access_token` 필드에 담아 응답합니다.
    
    * 리프레시 토큰처럼 Secure 옵션 등으로 보조할 수 없기 때문에, 응답이 HTTPS 환경에서만 수행되는지 따로 확인합니다.
        
* **보존**: 클라이언트에 결정을 맡깁니다.
    
    * 상태 관리 라이브러리 등 private 변수에서 관리합니다.
        
    * 새로고침 시 액세스 토큰이 사라지기 때문에 리프레시 요청을 보냅니다.
        
    * 많은 회사들은 이 방식 대신 local storage에 담기도 합니다.
        

## 리프레시 토큰 정리

* **생성**: 암호학적으로 안전한 랜덤 함수를 사용해, 충분한 길이로 생성합니다.
    
    * 예를 들어 16바이트(128비트) 또는 32바이트(256비트)를 선택할 수 있습니다.
        
* **전달**: 쿠키에 담아서 클라이언트에 전송합니다.
    
    * `HttpOnly; Secure; SameSite=Lax` 또는 `HttpOnly; Secure; SameSite=Strict` 옵션을 갖고 있는 쿠키에 담습니다.
        
* **보존**: 해싱을 하여 데이터베이스에 저장합니다.
    
    * 해싱의 강도는 비밀번호 암호화보다 낮춰 사용자 경험과 조절할 수 있습니다.
        
        * 리프레시 토큰이 완전히 랜덤으로 생성되며, 그 평균 수명이 대체로 짧고, 최대 수명까지 사용되는 리프레시 토큰도 적으며, 공격자의 오프라인 어택으로 1초에 수억 개씩 해싱을 시도해도 리프레시 토큰의 최대 수명까지 우연히 해독될 가능성이 매우 낮기 때문입니다.
            
    * 데이터베이스는 Redis 등 휘발성 데이터 보존에 편리한 것을 사용할 수 있습니다.
        
    * 데이터베이스의 종류는 상관이 없지만, 조회 성능이 좋은 것을 사용하는 것이 좋습니다.
        

---

이번에는 JWT 인증·인가 중 ‘인증(authorization)’의 일부인 토큰 발행을 집중적으로 정리했습니다. JWT 액세스 토큰을 발행하고, 리프레시 토큰을 생성해 우리 서버만 사용할 수 있는 쿠키에 담았는데요.

이 파트만 해도 낯선 분들께는 정리할 내용이 많은 만큼, 다음 글에서는 새로운 정보를 전달하기보다, 이 스펙에 맞춰 스프링 시큐리티 없이 구현하는 과정을 로직 중심으로 작성해 보겠습니다.
