[Spring Security] JWT 란? (AccessToken & RefreshToken)
🐥 JWT 시리즈
🪪 JWT란 무엇인가?
JWT(Json Web Token)는 JSON 포맷을 이용해 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다.
토큰 자체에 정보를 포함하는 Self-Contained 방식으로, 인증된 정보를 안전하게 전달하기 위해 주로 사용된다.
JWT는 헤더(Header), 페이로드(Payload), 그리고 서명(Signature)의 세 부분으로 구성되어 있으며, 이를 인코딩하여 하나의 문자열로 전달한다. 이때 서버는 서명을 통해 토큰의 위변조 여부를 검증할 수 있다.
❓ Self-Contained
말 그대로 “자체적으로 모든것을 포함하고 있다”는 의미다. 즉, JWT를 겁증하거나 사용하기 위해 별도의 서버 측 세션이나 DB를 참조할 필요가 없다는 뜻이다. 하지만 Refresh Token과 같이 사용할 때는 서버에서 일부 상태 저장이 필요할 수 있다.
🏗️ JWT 구조
위에서 설명한대로 JWT는 헤더(Header), 페이로드(Payload), 그리고 서명(Signature)의 세 부분으로 구성되어 있다. JSON 형태인 각 부분은 Base64로 인코딩되어 표현된다. 또한 각 부분은 . 구분자로 연결된다. 참고로 Base64는 암호화가 아니다. 단순히 바이너리 데이터를 문자열로 표현하기 위한 인코딩 방식이며, 같은 문자열에 대해 항상 같은 인코딩 문자열을 반환한다.
말로는 잘 이해가 안될 수 있으니 실제 JWT가 인코딩된 사진을 보자.

이제 Header, Payload, Signature 부분에 대해 자세히 살펴보도록 하자.
✔️ Header
헤더(Header)는 typ, alg 두 가지 정보로 구성되는데, typ은 토큰의 타입을 지정 하는 것이고 alg은 알고리즘 방식을 지정하며 Signature 및 토큰 검증에 사용된다.
1
2
3
4
{
"alg": "HS256",
"typ": JWT
}
✔️ Payload
페이로드(Payload)에는 토큰에서 사용할 정보의 조각들인 클레임(Claim)이 담겨 있다.
클레임은 등록된 클레임, 공개 클레임, 비공개 클레임으로 나눌 수 있으며, JSON 형식의 Key - Value 쌍으로 구성된다.
-
등록된 클레임(Registered Claim)
등록된 클레임은 JWT 표준에서 정의된 클레임으로, 토큰의 의미를 명확하게 하기 위해 사용된다. 예를 들어,iss는 토큰 발급자를 나타내고,exp는 토큰의 만료 시간을 나타낸다. -
공개 클레임(Public Claim)
공개 클레임은 사용자가 임의로 정의하여 추가하는 클레임으로, URI 포맷을 사용해 충돌을 방지한다. 예를 들어, 특정 시스템 내에서만 사용하는 사용자 정보를 저장할 수 있다. -
비공개 클레임(Private Claim)
비공개 클레임은 특정시스템 내에서만 의미가 있는 크레임으로, 다른 시스템에서는 이해할 수 있는 정보를 포함한다. 비공개 클레임 역시 사용자가 임의로 정의해 추가하는 클레임이다.
✔️ Signature
서명(Signature)은 JWT의 무결성을 보장하기 위한 값이다.
헤더와 페이로드를 각각 Base64로 인코딩한 후 연결한 문자열을, 헤더의 `alg` 크레임에 지정된 알고리즘(예: HS256)과 SecretKey를 사용해 해싱하여 생성한다. 이렇게 생성된 해시 값을 다시 Base64로 인코딩해 JWT의 마지막 부분에 추가한다.
서명 검증 시에는 수신된 JWT의 헤더와 페이로드를 추출해 동일한 방식으로 서명을 생성한 후 , 생성된 서명과 토큰에 포함된 서명을 비교한다. 두 서명이 일치하면 토큰의 무결성이 보장되는 것이다.

이렇게 생성된 JWT 토큰은 HTTP 통신을 할 때 Authorization이라는 key의 value로 사용된다. 일반적으로 value에는 Bearer이 앞에 붙여진다.
1
2
3
{
"Authorization": "Bearer {생성된 토큰 값}"
}
❓ Bearer
Authorization키의 값은 사실 <type> <credentials>로 구성되어 있다. 그러니깐 Bearer은 type에 해당되는 것이다. 토큰에는 많은 종류가 있고 서버는 다양한 종류의 토큰을 처리하기 위해 전송받은 type에 따라 토큰을 다르게 처리한다. Bearer 타입은 JWT 혹은 OAuth에 대한 토큰을 사용한다 (RFC 6750)
🧐 JWT vs 세션 vs 쿠키
🎟️ JWT
자 이제 JWT 인증/인가 흐름을 살펴보자 이 방식은 클라이언트가 인증 정보를 관리한다.
- 클라이언트가 서버에 HTTP 요청시 JWT 토큰을
Authorization헤더에 담아 요청한다. - 서버는 HTTP 요청의
Authorization헤더에서 JWT 토큰을 추출한다. - 서버는 JWT의 서명을 검증한다.
- 서명이 검증되면, JWT의 헤더와 페이로드가 변조되지 않았음을 확인하고, 서버는 페이로드에 담긴 사용자 정보로 인증/인가를 수행한다. (서명이 검증되지 않으면 요청은 거부된다.)

JWT 방식은 기본적으로 서버가 사용자 상태를 유지할 필요가 없어 Stateless하다. (서버 확장성 측면에서 유리)
서버에서 토큰을 검증하는 단계를 거쳐 보안상 쿠키 방식보다 뛰어나다는 장점이 있다.
하지만 토큰 탈취 위험이 존재한다. 이 위험을 해결해주는 것이 바로 Refresh Token이다.
💐 Access Token & Refresh Token
보통 JWT라고 하면, 인증 시 사용되는 Access Token을 의미한다. 그렇다면 Refresh Token이라고 하는 토큰 개념은 왜 생겨나게 되었는지 이해해 보자.
위에서 말했듯이 JWT 방식은 토큰 탈취 위험이 존재한다.
만약, 해커가 JWT Access Token을 탈취한다면?
해커는 탈취한 Access Token을 사용해 접근이 모든 가능해질 것이다.
이를 해결하기 위해 Access Token의 유효 기간을 짧게 하는 방법이 존재한다.
만약 해커가 탈취를 하더라도, 그 Access Token을 짧은 시간동안 밖에 못 쓰기 때문에 대응이 되는 것이다.
하지만, Acces Token의 유효 기간을 짧게 설정하면 사용자 입장에서는 매우 귀찮고 번거로울 것이다. 로그인 한지 얼마 되지도 않아 다른 페이지를 이동할 때 다시 로그인을 해서 Access Token을 발급 받아야하기 때문이다.
이러한 문제를 해결해주는 토큰 개념이 바로 Refresh Token이다. Refresh Token은 Access Token을 재발급 해주는 역할의 토큰이다.
Access Token과 Refresh Token 모두 JWT이지만, 서로 역할이 다르다.
-
Access Token:
처음 로그인 요청 시 서버에서 실제 유저의 정보가 담긴 Access Token을 발행한다.
클라이언트는 이 Access Token을 저장한 후, 요청마다 Access Token을 보내서 해당 Access Token을 서버에서 검증 후 유효하면 요청에 맞는 응답을 진행한다. -
Refresh Token:
처음 로그인 요청 시 서버에서 Access Token 재발급 용도인 Refresh Token을 발행한다.
이때, Refresh Token은 보통 서버 DB 또는 고유한 Redis 같은 저장소에 저장된다. 이는 클라이언트가 직접 보관하지 않도록 하여 보안성을 높인다. Refresh Token이 유효하면, Access Token의 재발급을 진행한다.
구체적으로 Refresh Token이 토큰 탈취 위험을 예방하는지 살펴보자.
Refresh Token의 유효 기간이 7일, Access Token의 유효 기간은 1시간 이라고 해보자.
Access Token이 1시간이 지나 만료후 클라이언트가 요청을 보낼 때 Refresh Token 로직이 추가되면, 서버에서는 인증 실패가 아닌 Refresh Token 검증 단계에 진입한다. Refresh Token이 유효하다면, 그 즉시 클라이언트에게 새로운 Access Token을 발행해주고 클라이언트는 그 Access Token을 받아 재요청을 하게 된다. 따라서 사용자의 눈에는 별도의 재로그인 과정없이 Access Token이 만료되지 않은 것처럼 동작하게 된다.
정리하자면, Access Token과 Refresh token 개념을 도입해서 Access Token의 유효 기간을 짧게 설정하고, Refresh Token을 사용해 Access Token을 재발급 받는 방식으로 보안을 강화한다.
🍪 쿠키
쿠키 인증/인가 방식은 클라이언트 측에서 인증 정보를 관리하는 방식이다.
인증/인가 흐름은 다음과 같다
- 클라이언트가 로그인 요청을 보내면, 서버에서 응답으로 쿠키에 사용자 인증 정보를 담아서 보낸다.
- 서버에서 응답한 쿠키를 클라이언트에서 저장하고, 인증/인가 요청 시마다 서버로 요청한다.

흐름만 보더라도 인증/인가 작업이 간단한 것을 알 수가 있다.
또 사용자의 인증 정보를 클라이언트가 관리하기 때문에 서버 부하가 적다는 장점이 있다.
각 요청마다 서버가 클라이언트의 상태를 기억할 필요가 없기 때문에 Stateless하다. 즉, 서버는 클라이언트가 누구인지 판단하기 위해 매요청마다 함께 전달되는 쿠키에 담긴 정보만 확인하면 되므로, 이전 요청과의 상태를 유지할 필요가 없다.
위와 같은 특징 및 장점이 있지만, 쿠키 인증 방식은 보안적으로 치명적인 단점이 존재하기 때문에 3가지 방식 중 거의 사용되지 않고 있다. 클라이언트(사용자)가 쉽게 쿠키에 담긴 인증 정보를 위조할 수 있기 때문에 XSS와 CSRF 공격에 노출될 위험이 있다.
⏳ 세션
세션 인증/인가 방식은 서버 측에서 세션으로 사용자의 인증 정보를 관리하는 방식이다.
인증/인가 흐름은 다음과 같다.
- 클라이언트가 로그인 요청을 보내면, 서버는 인증을 처리하고 세션을 생성한다. 생성된 세션은 서버에 저장되고, 세션 ID가 클라이언트에게 쿠키 형태로 전달된다.
- 클라이언트는 이후 요청마다 이 세션 ID를 서버에 전송해 인증/인가 요청을 보낸다. 서버는 해당 세션 ID에 연결된 인증 정보를 조회해 요청을 처리한다.

서버에서 세션을 통해 인증 정보를 관리하기 때문에 보안성이 높다는 장점이 있다. 또한 세션 관리를 통해 추가적인 기능(동시 접속 제한, 강제 로그아웃 등) 구현이 용이하다.
하지만 세션을 서버 측에 저장하므로 오히려 서버 부하가 상대적으로 클 수 있다는 단점이 있다. 특히 대규모 사용자 기반에서는 세션 저장소(메모리, DB 또는 Redis)를 사용해야 하며, 다중 서버 환경에서는 세션 동기화나 세션 클러스팅이 필요하다.
세션 인증에서 사용자 인증 정보는 보통 메모리, DB, 또는 Redis(인메모리 DB)에 저장된다. 하지만 모든 인증 정보를 DB에 저장하고 매 요청마다 DB를 쿼리하는 방식은 비효율적일 수 있으며, 연결 비용이 클 수 있다. 따라서 보통 서버 메모리나 Redis 같은 인메모리 DB를 이용해 빠르게 세션을 초회하는 방식이 사용된다.
또한, 세션 ID가 유출될 경우 세션 고정 공격 등의 보안 위험에 노출될 수 있다. 이를 방지하기 위해 세션 ID 재발급이나 HTTPS, HttpOnly, Secure 쿠키 속성 사용이 권장된다.
🔔 결론
결국 인증 방식을 세션과 JWT 기반 중 무엇을 선택해야 할까?
- 세션 기반 인증이 적합한 경우:
- 실시간으로 세션 상태를 관리해야 하는 경우
- 간단한 시스템으로 확장성이 크게 요구되지 않는 경우
- JWT 기반 인증이 적합한 경우:
- 높은 확장성과 유연성이 필요한 경우
- 모바일 앱이나 SPA(Single Page Application)와 같은 환경에서 사용해야 하는 경우
- 분산 환경에서 서비스를 제공해야 하는 경우
- CSRF 공격에 강한 시스템을 구축해야 하는 경우
JWT는 클라이언트 측에서 토큰을 저장하고 관리하므로, XSS 공격에 취약할 수 있다. 또한 세션 기반 인증도 적절히 구현하면 확정성 있는 시스템을 만들 수 있다.
결론적으로, 두 방식 모두 장단점이 있으며 시스템의 요구사항, 규모, 보안 요구 수준 등을 종합적으로 고려해 선택해야 한다.
++ JWT와 OAuth2를 구현하는 프로젝트를 진행하면서 OAuth2에 세션 방식과 토큰 방식 중 어떤 것을 선택할지 고민하게 되었다. 이 과정에서 Stack Overflow에서 해당 글을 발견했고 이를 통해서 앞으로의 프로젝트에 대한 방향성을 정할 수 있었다.
해당 글을 정리해보면 다음과 같다.
JWT는 서버 간의 인증을 위해 설계된 데이터 포맷이다. 다음과 같은 상황에서 유용하다.
- 서버 간 통신: 마이크로서비스 아키텍처에서 서비스 간 인증된 요청을 주고받을 때
- 일회성 요청: 비밀번호 재설정 링크와 같이 단일 사용을 위한 토큰이 필요한 경우
- 상태 비저장(Stateless) 인증이 요구되는 경우: 서버에서 세션 정보를 저장하지 않고도 인증을 처리해야 하는 상황
그러나 JWT를 일반적인 사용자 인증에 무분별하게 적용하는 것은 여러 가지 보안 문제를 초래할 수 있다.
- XSS(Cross-Site Scripting) 취약점:
- JWT를 클라이언트 측에 저장할 경우, XSS 공격으로 인해 토큰이 탈취될 위험이 있다
- 공격자가 악성 스크립트를 주입하여 저장된 JWT를 읽어갈 수 있다.
- 안전하지 않은 저장 방식:
- JWT를 로컬 스토리지에 저장하는 것은 OWASP(Open Web Application Security Project)에서 강력히 권장하지 않는다.
- 로컬 스토리지는 JavaScript를 통해 쉽게 접근할 수 있어, XSS 공격에 매우 취약하다.
- 로그아웃 처리의 어려움:
- JWT는 기본적으로 상태 비저장(stateless) 방식이기 때문에, 서버에서 토큰을 즉시 무효화하기 어렵다.
- 완전한 stateless 방식으로는 사용자 로그아웃을 구현하기 어려우며, 추가적인 메커니즘(예: 블랙리스트)이 필요하다.
- 다중 기기 로그아웃 문제:
- 세션 기반 인증과 달리, JWT를 사용할 경우 모든 기기에서 동시에 로그아웃하는 기능을 구현하기가 복잡하다.
- 각 기기마다 발급된 토큰을 개별적으로 관리해야 하는 부담이 있다.
따라서 앞으로 진행할 개인 프로젝트들에서는 서버 간 통신이 없는 일반적인 웹 애플리케이션인 경우에는 세션 기반 인증 방식을 채택하고 서버 간 통신이나 특정 마이크로서비스 아키텍처에서만 제한적으로 JWT 사용하도록 하겠다. 또한 토큰의 유효 기간을 짧게 설정하고 리프레시 토큰 매커니즘을 구현해 보안을 강화하겠다.
참고
Leave a comment