토큰 관리 방식 톺아보기
0. 들어가며 🎯 서비스에 접근하려는 사용자가 누구인지 확인하는 과정을 사용자 인증이라고 합니다. 인증된 사용자에게 주어진 권한을 확인하는 작업은 인가라고 부릅니다. 이번 글에서는 인가는 다루지 않습니다. 사용자 인증에는 많은 방식이 있지만, 오늘은 세션 인증 방
Reference: https://olrlobt.tistory.com/98
0. 들어가며 🎯
사용자 인증에는 다양한 방식이 있지만, 오늘은 세션 인증 방식과 토큰 인증 방식에 대해 다루고자 합니다. 특별히, 토큰 인증 방식에서 Access Token과 Refresh Token을 관리하는 방식에 집중할 예정입니다.
많은 포스팅에서는 쿠키 인증 방식을 별도로 다루곤 합니다. 그러나 쿠키는 데이터 전달을 위한 매개체입니다. 세션 ID를 쿠키에 담으면 세션 인증 방식이고, 토큰을 쿠키에 담으면 토큰 인증 방식이 되는 식입니다. 따라서 해당 포스팅에서는 쿠키를 인증 방식으로서 별도로 다루지는 않을 생각입니다.
1. 세션 인증 방식 🎯
세션 인증 방식의 핵심은 Stateful입니다. Stateful은 달리 표현하면, 서버에 상태를 저장하는 행위라고 할 수 있습니다.
사용자가 서버로 로그인 요청을 전송하면 서버는 요청 정보가 Database와 일치하는지 파악한 후 사용자 세션을 생성합니다. 세션을 생성하면 해당 세션에 대한 세션 ID가 생성됩니다. 세션 ID를 메모리나 Database와 같은 서버의 저장소에 저장하기 때문에 Stateful이라고 부릅니다.
사용자는 후속 요청마다 쿠키에 세션 ID를 담아 서버에 전송하고, 서버는 세션 ID가 저장소에 존재하는지 확인하는 방식으로 사용자를 검증합니다.

서버에서 인증을 관리하기에, 클라이언트 단에서 정보가 노출될 위험이 적다는 보안적인 장점이 있습니다.
그러나 사용자의 요청마다 매번 Database까지 접근해야 하니 응답 속도가 떨어집니다. 요청의 수가 늘수록 서버에 부하를 일으키게 되죠. 게다가 요즘은 Microservice Architecture를 도입하는 서비스가 많은데요, 세션의 일관성을 유지하기 위해 중앙 저장소를 별도로 구축하는 식의 추가적인 작업이 요구됩니다. 확장성이 떨어지는 것입니다.
2. 토큰 인증 방식 🎯
토큰 인증 방식의 핵심은 Stateless입니다. Stateless는 서버에 상태를 저장하지 않는 것으로 이해할 수 있습니다.
세션 인증 방식의 단점은 응답 속도 저하, 서버 부하 증가, 확장성의 한계로 요약할 수 있습니다. 반면, 토큰 인증 방식은 Database까지 거치지 않고도 토큰만을 활용하여 인증을 진행하기 때문에, 응답 속도 저하 이슈를 해결할 수 있으며 확장성도 높게 평가할 수 있습니다.

대신, 토큰은 탈취 위험이 있습니다. 클라이언트 자체에서 탈취되거나, XSS 등으로 탈취될 가능성도 있으며, 클라이언트에서 서버로 전송되는 과정에서도 탈취될 수 있습니다.
3. Access Token 🎯
3-1. Access Token과 JWT ✅
Access Token은 만료 기한이 있는 문자열입니다. 발급되는 토큰 종류에 따라 구조화된 형태일 수도 아닐 수도 있습니다.
JWT(JSON Web Token) 형식은 Stateless 하다는 장점 덕분에 주로 사용됩니다. Stateless 하다는 것은, 서버에서 Access Token을 저장할 필요가 없다는 것입니다. 우선 JWT의 구조를 살펴보겠습니다.

헤더에는 토큰의 유형과 서명 알고리즘에 대한 정보가 있습니다.
페이로드에는 사용자 ID, 권한, 만료 시간 등의 정보가 포함됩니다. 페이로드 덕분에 서버가 별도의 Database 조회 없이도 사용자의 정보를 파악할 수 있는 것입니다. 이러한 특징을 Self-contained라고 합니다.
마지막으로 서명은, 헤더와 페이로드를 조합한 후, 헤더에 명시된 서명 알고리즘과 비밀키를 사용하여 생성합니다. 서명을 통해 변조되지 않은, 신뢰할 수 있는 토큰임을 보장할 수 있게 됩니다.
헤더, 페이로드, 서명이 결합되어 하나의 JWT 토큰이 됩니다. JWT 자체에 필요한 모든 정보와 변조 감지를 위한 서명이 포함되어 있기에 서버에서 별도의 세션 저장소를 유지할 필요가 없어지게 됩니다.
클라이언트가 JWT만 가지고 있다면 인증이 되는 것인데요, 그렇다면 클라이언트는 JWT를 어떻게 저장하고 관리하는지 궁금합니다.
3-2. Access Token 저장 위치 ✅
Local Storage
로컬 스토리지는 클라이언트 측에 데이터를 영구적으로 저장하는 방법입니다. 브라우저를 닫아도 데이터가 유지되고, 명시적으로 삭제하지 않는 한 계속 존재합니다. XSS는 자바스크립트 기반의 악성코드를 브라우저에서 실행되도록 하는 공격 방식인데요, XSS에 의해 로컬 스토리지에 저장된 Access Token이 탈취될 위험이 있습니다. 따라서 Access Token 저장 위치로 권장되지 않습니다.
Session Storage
세션 스토리지 역시 클라이언트에 데이터를 저장하는 방식이지만, 브라우저가 닫히면 데이터가 삭제되며, 동일한 탭에서만 유효하다는 특징이 있습니다. 데이터가 계속해서 브라우저에 남아있지 않아 로컬 스토리지에 저장하는 것에 비해서는 보안 측면에서 안전하다고 볼 수 있지만, 여전히 XSS 공격에 취약하기에 권장되지 않는 방식입니다.
Cookies
쿠키는 모든 클라이언트 요청에 자동적으로 포함됩니다. 쿠키는 데이터를 전달하는 매개체에 불과합니다. 앞서 언급했던 보안 위협들을 쿠키가 어떻게 예방할 수 있는지를 아는 것이 중요합니다.
HttpOnly 플래그로, 자바스크립트를 통한 쿠키 접근을 막을 수 있습니다. 즉 XSS 공격을 방어할 수 있습니다. 또한 Secure 플래그로, HTTPS 연결에서만 쿠키가 전송되게 하여 MITM(중간자 공격)으로부터 쿠키를 보호할 수 있습니다. 마지막으로, SameSite 속성을 설정하면 CSRF(교차 사이트 요청 위조) 공격을 방어할 수 있습니다.
Memory
메모리에 Access Token을 저장하는 방식은, 로컬 스토리지나 세션 스토리지와 달리 메모리에만 존재하므로 악성 스크립트가 토큰에 접근하기 어렵고, 쿠키와 달리 모든 요청에 자동으로 포함되지 않기 때문에 CSRF 공격에 노출되지 않는다는 장점이 있습니다.
그러나, 사용자가 브라우저를 새로고침하면 토큰이 사라지기 때문에, 다시 로그인하거나 Refresh Token을 이용해 재발급 받아야 합니다. 브라우저를 닫으면 토큰이 사라지므로, 지속적인 로그인 유지에는 적합하지 않습니다.
3-3. Access Token 사용자 인증 방식 ✅

사용자가 로그인 정보를 입력하고 서버에 로그인 요청을 보내면, 서버는 사용자의 인증 정보를 Database와 일치하는지 확인한 후, JWT를 생성하여 클라이언트에 반환합니다.
후속 요청에서는, 클라이언트가 서버에 요청을 보낼 때 헤더에 Access Token을 포함합니다. 쿠키의 경우에는 자동으로 포함되겠죠. 서버는 요청받은 Access Token에서 사용자의 인증 상태를 확인합니다.
요청이 수천 번 일어나더라도, 서버에서 사용자 인증을 위해 Database에 접근하는 부분은, JWT를 발급하는 로그인 부분뿐입니다. JWT의 Stateless 함이 가장 잘 드러나는 부분이라고 볼 수 있습니다.
3-4. Access Token 로그아웃 방식 ✅
Memory
클라이언트 private 변수에 Access Token을 저장한 경우에는, Access Token 자체를 제거해 주면 됩니다. 간단히 null 처리를 통해 로그아웃을 구현할 수 있겠습니다.
HttpOnly Cookie
HttpOnly 쿠키의 경우 자바스크립트로 직접 쿠키를 조작할 수 없습니다. 따라서 클라이언트가 쿠키를 직접 수정하는 것이 아니라, 서버에 쿠키를 만료시키라는 지시를 보내야 합니다. 서버가 클라이언트로부터 로그아웃 요청을 받으면, Set-Cookie 헤더에 있는 Expires 또는 Max-Age 속성을 통해 쿠키를 제거하도록 할 수 있습니다. 브라우저는 쿠키가 삭제되었기에 더 이상 Access Token을 사용할 수 없게 됩니다. 즉, 로그아웃에 성공한 것이죠.
BlackList
서버에서 발급한 JWT를 블랙리스트에 추가하여 해당 토큰의 유효성을 없앨 수도 있습니다. 다만, 추가적인 저장소를 필요로 한다는 점에서 Stateless 함을 잃는다고 볼 수 있고, 매번 블랙리스트에 등재된 Access Token 인지 검증해야 할 필요가 생기므로 오버헤드가 발생합니다.
3-5. Access Token 탈취 ✅
그런데 Access Token 만으로 인증을 처리하는 과정에서 해커가 Access Token을 탈취한다면 어떻게 될까요. Access Token이 탈취되면, Access Token으로 접근한 요청이 사용자의 요청인지 해커의 요청인지 구분할 수 없게 됩니다.

Access Token의 만료 시간이 다 될 때까지는 아무런 조치도 취할 수 없습니다. 게다가, Access Token 하나만을 인증에 사용 중이라면, 사용자 편의를 위해 긴 만료 시간을 갖고 있을 것입니다. BlackList에 추가하는 것 역시 이상 패턴이 발견된 후의 이야기이므로 피해를 막을 수는 없습니다.
그렇다면, Access Token의 만료 시간을 짧게 하면 어떨까요. 쇼핑 한 번 하는데 로그인을 10분마다 해야 되면 누가 우리 서비스를 사용할까요. 이러한 딜레마로부터 Refresh Token의 개념이 도입되었습니다.
4. Refresh Token 🎯
4-1. Refresh Token과 JWT ✅
Refresh Token은 Access Token처럼 JWT 형식으로 만드는 것이 일반적입니다. JWT로 만들면 토큰 자체만으로 "이 토큰이 유효한가?"를 1차 검증할 수 있고, 토큰 안에 담긴 최소한의 정보를 꺼내서 DB를 조회한 뒤 새로운 Access Token을 발급할 수 있습니다.
그런데, JWT는 원래 토큰 안에 필요한 정보를 다 넣어두고 DB 없이도 모든 처리를 끝낼 수 있는 Self-contained이 큰 장점입니다. Refresh Token에도 동일한 방식을 적용하면 DB 조회 없이 빠르게 처리할 수 있습니다.
하지만 그렇게 하면 Access Token 재발급이라는 Refresh Token 본래의 역할을 넘어서는 불필요한 정보까지 토큰에 담기게 되고, 토큰이 탈취됐을 때 DB에서 강제로 무효화하는 것이 불가능해지는 보안 문제가 생깁니다.
그래서 Refresh Token을 JWT로 만들더라도, "DB 없이 자체적으로 모든 걸 해결한다"라는 특성은 쓰지 않고 반드시 DB 조회를 거치는 방식으로 구현하는 것이 권장됩니다.
4-2. Refresh Token 저장 위치 ✅
Refresh Token은 주로 Database에 저장됩니다. 응답 속도를 빠르게 하기 위해서 Redis를 활용합니다. 무효화 가능성은 DB의 안전함으로, 조회 속도는 Redis의 빠름으로 취하는 것이죠.
로컬 스토리지와 세션 스토리지는 XSS 공격의 취약점이 크기 때문에 Access Token에서도 권장하지 않는 방식이라고 소개했습니다. 추가적으로, 메모리의 경우 페이지 이동과 새로고침 시에 소멸하는 문제가 있어 Refresh Token에는 적합하지 않습니다.
쿠키보다는 서버가 보안적으로 더 우수하고, 서버보다는 쿠키가 더 Stateless 하다는 토큰 인증 방식의 특징을 잘 살린다고 볼 수 있습니다. Access Token과 Refresh Token의 작동 방식을 학습하며 조금 더 고민해 보죠.
5. Access Token과 Refresh Token의 작동 방식 🎯

쿠키 방식
로그인 시 Access Token과 Refresh Token을 모두 쿠키에 저장합니다.
후속 API 요청마다 두 토큰을 함께 전달하고, 서버에서는 Access Token의 만료를 확인하고 곧바로 Refresh Token의 만료를 확인하는 순으로 검증합니다.
Access Token이 만료되었다면, Refresh Token ID로 사용자 정보 요청을 진행한 뒤, 해당 응답으로 Access Token을 재발급합니다.
서버 방식
로그인 시 Access Token만 클라이언트에 주고, Refresh Token은 서버에 저장합니다.
API 요청은 Access Token 만으로 진행하다가 만료되면 클라이언트가 Refresh Token을 별도로 요청하고, 서버는 저장된 Refresh Token과 대조 검증 후 Access Token을 재발급합니다.
로그아웃 방식
Refresh Token의 로그아웃 방식은 Access Token과 동일하여 생략하겠습니다.
6. Refresh Token 탈취 🎯
Refresh Token은 Access Token을 재발급할 수 있기에, 해커가 Refresh Token을 탈취했고 서버 측에 무효화 로직이 없다면, 이어지는 공격을 막을 수 없습니다.

Refresh Token이 탈취당했을 때 피해를 최소화하기 위하여 RTR과 같은 기법을 도입합니다.
6-1. RTR(Refresh Token Rotation) ✅
Refresh Token의 만료 기간을 짧게 가져가면 어떨까요. Refresh Refresh Token을 도입해야 할까요. Refresh Token Rotation 기법을 살펴보겠습니다. RTR은, 보안을 강화하기 위해 Refresh Token을 사용하여 Access Token을 갱신할 때마다 기존 Refresh Token은 폐기하고 새로운 Refresh Token을 발급하는 방식을 의미합니다.

이미지의 좌측과 우측 모두 해커가 Refresh Token(RT1)을 탈취한 상황입니다.
다만 좌측 이미지는, 사용자가 먼저 Refresh Token(RT1)을 사용하여 재발급 요청을 보낸 상황입니다. Access Token(AT2)과 Refresh Token(RT2)을 재발급함과 동시에 기존 Refresh Token(RT1)을 블랙리스트에 저장합니다. 해커가 Refresh Token(RT1)을 통해 토큰 재발급 요청을 보내면, 서버에서는 블랙리스트 확인을 통해 해당 Replay Attack을 감지한 후 모든 Refresh Token(RT1, RT2)을 무효화합니다.
우측의 경우, 해커가 먼저 Refresh Token(RT1)을 사용하여 재발급 요청을 보냈습니다. 서버는 마찬가지로 Access Token(AT2)과 Refresh Token(RT2)을 재발급함과 동시에 기존 Refresh Token(RT1)을 블랙리스트에 저장합니다. 사용자가, 이미 탈취당한 Refresh Token(RT1)을 통해 토큰 재발급 요청을 보내면, 서버에서는 블랙리스트 확인을 통해 해당 Replay Attack을 감지한 후 모든 Refresh Token(RT1, RT2)을 무효화합니다.
요컨대, RTR은 해커든 사용자든 Refresh Token에 대한 Replay Attack이 감지되면, 모든 토큰을 무효화하는 방식으로 보안을 강화합니다.
7. Access Token과 Refresh Token의 저장 위치 조합 🎯
Access Token은 메모리에, Refresh Token은 HttpOnly 쿠키에 저장하며 Refresh Token Rotation 방식을 적용하는 조합을 고려해 봤습니다.
로그인
사용자가 ID와 비밀번호를 입력하면 요청은 HTTPS를 통해 서버로 전달됩니다. TLS가 적용된 HTTPS 환경에서는 MITM으로 전송 중인 데이터를 가로채는 것이 사실상 불가능합니다. 서버는 전달받은 자격 증명을 데이터베이스에서 조회해 비밀번호 해시를 비교하고 사용자를 인증합니다.
토큰 발급
인증이 완료되면 서버는 Access Token(AT)과 Refresh Token(RT)을 생성합니다. AT는 클라이언트의 메모리에, RT는 HttpOnly 쿠키에 저장됩니다. HttpOnly 쿠키는 document.cookie로 읽을 수 없어 XSS 공격이 성공하더라도 RT는 탈취되지 않습니다. 설령 XSS가 발생해도 AT만 노출될 뿐이고, AT는 유효 기간을 짧게 설정해 피해 범위를 최소화합니다. RT는 서버에서 해시값과 폐기 여부와 함께 데이터베이스에 저장됩니다.
API 요청(stateless)
클라이언트는 매 API 요청마다 Authorization 헤더에 AT를 담아 전송합니다. 서버는 토큰 안에 담긴 클레임 만으로 요청을 검증하기 때문에 데이터베이스를 조회할 필요가 없습니다. stateless 인증의 핵심으로, 서버 부하를 줄이고 서버를 여러 대로 늘리는 수평 확장도 훨씬 쉬워집니다.
AT 만료 → RT로 재발급
AT가 만료되면 클라이언트는 RT 쿠키를 /auth/refresh 엔드 포인트로 자동 전송합니다. RT 쿠키에는 Path=/auth/refresh 속성이 설정되어 있어 일반 API 트래픽에는 RT가 실리지 않습니다. 쿠키 기반 전송에서 우려되는 CSRF 공격은 SameSite=Strict 또는 SameSite=Lax 속성으로 차단합니다. 서버는 데이터베이스에서 RT의 폐기 여부를 확인한 뒤 새 토큰 발급 여부를 결정합니다.
RTR(Refresh Token Rotation)
재발급 요청이 유효하면 서버는 새 AT와 RT를 생성하고 기존 RT를 즉시 폐기합니다. 새 RT는 데이터베이스에 저장되고 클라이언트의 HttpOnly 쿠키를 교체합니다. 새 AT는 클라이언트 메모리에 저장되어 이후 API 요청에 사용됩니다.
Reuse Detection
이미 폐기된 RT로 재발급 요청이 들어오면 서버는 이를 토큰 탈취로 판단합니다. 이 시점에 서버는 해당 사용자의 모든 RT를 데이터베이스에서 일괄 폐기하고 전체 세션을 강제 만료시킵니다. 클라이언트는 강제 로그아웃 처리되며, 공격자가 탈취한 토큰은 더 이상 사용할 수 없게 됩니다. RTR과 Reuse Detection이 함께 적용될 때 비로소 토큰 탈취 시나리오까지 대응하는 완전한 방어 구조가 완성됩니다.
More to read
프론트엔드와 백엔드 사이
HTTP 상태 코드는 프론트엔드에서 백엔드로 보냈던 요청의 수행 결과를 의미하는 일종의 약속이며, API를 구성하는 핵심 요소 중 하나입니다. 상태 코드와 관련하여, 백엔드는 잘 모르는 프론트엔드의 슬픈 사정이 있습니다.아래는 요청이 실패했음에도, 백엔드에서 상태 코드
A2AA2A / MCP 멀티 에이전트 오케스트레이션
0. 들어가며 ✍️ Google for Developers에, 레스토랑 공급망 시나리오로 엮은 6대 프로토콜(MCP, A2A, UCP, AP2, A2UI, AG-UI)에 대한 가이드가 게시된 이후, MCP와 A2A부터 구현해 보는 것이 좋을 것 같다는 생각이 들었습니
'UCP'AI Agent 핵심 프로토콜 완전 정복: MCP부터 AG-UI까지
0. 들어가며 ✍️ Reference: https://developers.googleblog.com/developers-guide-to-ai-agent-protocols/ 최근 AI Agent 개발 분야는 MCP, A2A, UCP, AP2, A2UI, AG-UI 등 수많은 약어로 인해 혼란이 가중되고 있습니다. 바로 어제, Google for Dev...