최근 티켓 예매 프로젝트(Ticket Run)에서 소셜 로그인 이후의 인증을 JWT로 구현하면서 Access Token과 Refresh Token을 함께 발급하게 되었다.
처음 접하면 "토큰 하나로 인증하면 되는데 왜 두 개나 필요할까?"라는 의문이 든다. 둘 다 서버가 발급하는 토큰이고, 둘 다 만료 시간이 있기 때문이다.
하지만 핵심 차이는 Access Token은 API를 호출할 때 쓰고, Refresh Token은 Access Token을 다시 받을 때만 쓴다는 점이다.
이 글에서는 Access Token만 쓸 때 생기는 문제를 기준으로 두 토큰이 무엇이고 왜 함께 쓰는지를 정리한다(저장 위치는 다음 글에서 다룬다).
간단히 정리하면
- Access Token : 보호된 API에 접근할 때 매 요청마다 보내는 자격 증명
- Refresh Token : Access Token을 새로 발급받을 때만 인가 서버에 보내는 자격 증명
API 호출 → Access Token (짧게, 예: 30분)
만료 후 재발급 → Refresh Token (길게, 예: 14일)
Access Token
Access Token은 보호된 리소스(API)에 접근할 때 사용하는 자격 증명(credentials)이다. (RFC 6749, Section 1.4)
예를 들어 사용자가 내 정보를 조회하면, 클라이언트는 요청마다 Access Token을 헤더에 담아 보낸다.
요청은 다음처럼 들어온다.
GET /api/members/me HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiJ9...
RFC는 Access Token의 형식을 정하지 않지만, Ticket Run에서는 JWT(JSON Web Token)로 만든다. JWT는 회원 ID, 만료 시각(exp) 같은 정보를 서명과 함께 토큰 안에 담는다. (RFC 7519, RFC 9068)
그래서 서버는 세션 저장소나 DB를 조회하지 않고 서명과 exp만 검증해 요청자를 확인할 수 있다. 세션 방식과 달리 서버가 로그인 상태를 들고 있지 않아도 된다는 것이 토큰 방식을 쓰는 이유다.
다만 이 장점이 그대로 약점이 된다. 서버가 상태를 저장하지 않으니, 이미 발급한 토큰을 서버가 중간에 끊을 방법이 마땅치 않다.
- 로그아웃해도 발급된 토큰은 만료 전까지 유효하다.
- 토큰이 탈취되면 공격자도 만료 전까지 정상 사용자처럼 요청할 수 있다.
그렇다면 만료 시간을 짧게 하면 되지 않을까?
만약 Access Token 만료가 10분이라면 탈취 피해는 10분으로 줄어든다. 대신 사용자는 10분마다 다시 로그인해야 한다.
반대로 만료를 2주로 늘리면 로그인은 편해지지만, 탈취된 토큰도 2주 동안 살아 있다.
즉, Access Token 하나로는 보안과 사용자 편의 중 하나를 포기해야 한다.
Refresh Token
Refresh Token은 Access Token을 새로 발급받는 데만 사용하는 자격 증명이다. (RFC 6749, Section 1.5)
예를 들어 30분짜리 Access Token이 만료되면, 클라이언트는 사용자에게 다시 로그인을 요구하지 않고 Refresh Token으로 새 Access Token을 받는다.
RFC 6749의 흐름(Figure 2)을 단순화하면 다음과 같다.
- 로그인: 인가 서버(Authorization Server)가 Access Token과 Refresh Token을 발급한다.
- API 요청: 클라이언트는 리소스 서버(Resource Server)에 Access Token만 보낸다.
- 만료 응답: Access Token이 만료되면 리소스 서버가 거절한다.
- 재발급 요청: 클라이언트가 인가 서버에 Refresh Token을 보낸다.
- 재발급: 인가 서버가 Refresh Token을 검증하고 새 Access Token을 발급한다.
Ticket Run에서는 두 서버를 하나의 Spring 서버가 맡고, 4단계가 /api/auth/reissue 호출에 해당한다.
중요한 점은 Refresh Token이 인가 서버에만 전달되고 리소스 서버로는 절대 보내지 않는다는 것이다. (RFC 6749, Section 1.5)
자주 오가는 토큰은 수명을 짧게 두고, 수명이 긴 토큰은 드물게만 보낸다. 이렇게 역할을 나누면 앞에서 본 두 문제를 함께 줄일 수 있다.
탈취 피해 → Access Token 수명이 짧아 피해 시간이 줄어든다
재로그인 → Refresh Token 수명이 길어 로그인을 자주 하지 않는다
또한 Refresh Token은 서버가 저장소에 보관해 관리할 수 있다.
그래서 로그아웃 같은 상황에서 서버가 저장된 Refresh Token을 지우면 재발급이 바로 막히고, 남은 Access Token도 만료되는 순간부터 더 이상 이어지지 않는다. RFC 9700도 로그아웃 같은 보안 이벤트에서 인가 서버가 Refresh Token을 폐기할 수 있다고 설명한다. (RFC 9700, Section 4.14.2)
즉, Access Token은 상태 없이 빠르게 검증하고, 끊어야 할 때는 Refresh Token 쪽에서 끊는다.
다만 Refresh Token 자체가 탈취되면 공격자도 Access Token을 계속 재발급받을 수 있다. 수명이 긴 만큼 위험도 크다.
Refresh Token Rotation
추가적으로 Refresh Token 탈취에 대응하는 토큰 회전(Refresh Token Rotation, RTR)을 알아보려고 한다!
Ticket Run의 재발급 API도 Refresh Token 탈취 위험 때문에 RTR 방식으로 구현했다.
RTR은 재발급할 때마다 Refresh Token도 새로 발급하고, 이전 Refresh Token은 무효화하는 방식이다. (RFC 9700, Section 4.14.2)
예를 들어 공격자가 Refresh Token을 훔쳐 먼저 재발급을 받았다고 가정한다. 이후 정상 사용자가 이미 무효화된 이전 토큰으로 재발급을 요청하면, 서버는 같은 토큰이 두 번 쓰였다는 것을 알아챌 수 있다.
이때 서버는 해당 사용자의 Refresh Token을 모두 폐기한다. 공격자가 가진 토큰도 함께 무효화된다.
중요한 점은 RTR이 탈취를 막는 것이 아니라 탈취를 탐지하고 피해를 끊는 장치라는 것이다. 대신 정상 사용자도 다시 로그인해야 하는 비용이 따른다.
정리
두 토큰은 모두 서버가 발급한 자격 증명이라는 공통점이 있다. 차이는 어디로 보내고 얼마나 오래 사느냐에 있다.
| 구분 | Access Token |
Refresh Token |
|---|---|---|
| 용도 | API 접근 | Access Token 재발급 |
| 전송 대상 | 리소스 서버 (모든 API 요청) | 인가 서버 (재발급 요청만) |
| 수명 | 짧다 (예: 30분) |
길다 (예: 14일) |
| 서버 검증 | 서명, exp 검증 (저장소 조회 없음) |
저장소의 값과 비교 |
| 무효화 | 만료 전에는 어렵다 | 저장소에서 지우면 재발급이 즉시 막힌다 |
매 요청 인증 → Access Token, 짧게
로그인 상태 유지 → Refresh Token, 길게
탈취 대응 → 저장된 Refresh Token 삭제, RTR
만료 시간은 정답이 정해진 값이 아니다. 서비스가 감당할 수 있는 탈취 피해 시간과 재로그인 빈도를 기준으로 정한다.