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

Merge SimpsonMerge Simpson
5 min read

이전 글에서 정리에 꽤 힘을 뺐기 때문에, 이번 글에서는 서두와 부연설명을 줄이고 필요한 정보를 담아 전달해 보겠습니다.


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 액세스 토큰을 발행하고, 리프레시 토큰을 생성해 우리 서버만 사용할 수 있는 쿠키에 담았는데요.

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

30
Subscribe to my newsletter

Read articles from Merge Simpson directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Merge Simpson
Merge Simpson

Hello, I am Korean. Welcome, visitor. You are very cool. 안녕하세요, 저는 한국어입니다. 방문자여 환영한다. 당신은 매우 시원해.