
Refresh Token(리프레시 토큰) 이란, 만료된 Access Token(액세스 토큰)을 새로 발급받기 위해 사용하는 장기 인증용 토큰이다.
보안성강화와 사용자 편의성을 위해, 유효기간이 정해져 있고 이를 갱신해야 사용할 수 있다.
Access Token은 서버측에서 관리할 수 있다.
그치만 오늘의 주제는 프론트엔드 관점이므로, 자세히 살펴보지 않는다.
가장 단순한 방법. Refresh Token을 그냥 자바스크립트 변수(또는 React state/context)에 담아두는 것이다.
const [refreshToken, setRefreshToken] = useState(null);
장점
단점
갱신 방법
{ refreshToken })에 담아 재발급 엔드포인트로 전송한다. 응답으로 전달받은 신규 Refresh Token을 상태 업데이트 함수(setRefreshToken)를 통해 메모리 변수에 재할당한다.서버가 Set-Cookie 헤더를 통해 브라우저 쿠키에 토큰을 저장하도록 지시하는 방식이다. (가장 권장됨)
장점
HttpOnly 설정 시 XSS 방어: JavaScript(document.cookie)를 통한 접근이 차단되어 악성 스크립트로 토큰을 탈취할 수 없다.단점
| 속성 | 방어 대상 및 역할 |
|---|---|
HttpOnly | XSS 방어: JavaScript 접근 차단 |
Secure | 중간자 공격(MITM) 방어: HTTPS 암호화 통신 채널에서만 전송 허용 |
SameSite=Strict / Lax | CSRF 방어: 타 사이트에서의 교차 요청 제어 (Strict: 전면 차단, Lax: GET 등 안전한 탐색 시 일부 허용) |
갱신 방법
credentials: 'include'(또는 withCredentials: true) 옵션에 따라 자동으로 함께 전송한다. 서버는 검증 후 새로 생성된 Refresh Token을 응답의 Set-Cookie 헤더로 재발급하여 브라우저 내 쿠키를 덮어쓴다. (프론트엔드 코드에서 쿠키 값을 직접 조작하지 않음)웹 스토리지 API를 활용하여 브라우저의 단일 탭(세션) 내에만 토큰을 보관하는 방식이다.
장점
단점
localStorage와 동일하게 JavaScript로 접근 가능하므로 sessionStorage.getItem()을 통한 XSS 탈취 위협을 방지하지 못한다.갱신 방법
sessionStorage.getItem('refreshToken')으로 토큰을 불러와 재발급 API 요청을 보낸 후, 응답으로 수신한 신규 토큰을 sessionStorage.setItem('refreshToken', newRefreshToken)으로 현재 탭 내 스토리지에 덮어써서 업데이트한다.웹 스토리지 API를 활용하여 브라우저 내 영구 저장 공간에 토큰을 보관하는 방식이다.
장점
getItem, setItem)로 손쉽게 제어 가능하다.단점
localStorage.getItem('refreshToken')으로 토큰이 즉시 탈취된다.갱신 방법
localStorage.getItem('refreshToken')으로 기존 토큰을 추출해 재발급 API 요청 바디/헤더로 전송한다. 서버 응답으로 전달받은 신규 토큰을 localStorage.setItem('refreshToken', newRefreshToken) 명령어로 덮어써서 업데이트한다.크로스 플랫폼이나 모바일 호환성을 고려하지 않아도 되는 웹 프론트엔드에서 JWT 토큰을 이용한 로그인을 구현한다면, 액세스 토큰과 리프레시 토큰은 쿠키에 저장할 것입니다.
JavaScript를 통한 직접적인 접근을 차단하여 악성 스크립트 주입 등 XSS 공격을 통한 토큰 탈취 위험을 최소화할 수 있고, 브라우저에서 쿠키를 자동으로 관리해주기 때문에 별도의 토큰 저장 로직을 구현할 필요가 없기 때문입니다.
클라이언트가 로그인을 요청하면, 서버는 먼저 회원가입 여부와 비밀번호를 검증합니다. 인증에 성공하면 짧은 수명의 Access Token과 긴 수명의 Refresh Token을 동시에 생성해 쿠키로 응답하며, 이때 Refresh Token은 서버의 Redis 등에도 함께 저장해둡니다. 이후 API 요청 시 브라우저가 쿠키를 자동으로 첨부하고, Access Token이 만료되어 서버로부터 401 응답을 받으면, 프론트엔드는 저장되어 있던 Refresh Token 쿠키를 이용해 Access Token을 재발급받습니다. 다만 두 토큰을 모두 HttpOnly 쿠키에 저장할 경우 프론트엔드 JS 코드가 Access Token의 만료 여부를 직접 확인할 수 없기 때문에, 401 응답을 감지해 자동으로 재발급을 요청하는 Axios Interceptor 로직과 함께 서버 측에서 재발급 API(/refresh)를 Access Token 검증 대상에서 제외해 주는 설정이 필요합니다. 또한 로그인 상태를 화면에 표시하기 위해서는, 로그인 성공 시 HttpOnly가 아닌 별도의 상태 확인용 쿠키나 응답 값을 함께 내려주는 방식을 고려할 수 있습니다.
이와 같은 방식으로 JWT 로그인을 구현하면, HttpOnly 쿠키로 XSS를 통한 직접적인 토큰 탈취는 막을 수 있지만, CSRF, 네트워크 스니핑, 기기 탈취 등 다른 경로를 통한 토큰 유출을 완벽히 차단할 수는 없으며, 로그인·재발급 API에 대한 무차별 대입 공격이나 비정상적인 반복 요청도 방어할 수 없다는 한계가 있습니다.
이를 위해 HttpOnly, Secure 속성과 함께 CSRF 방어 및 일반 탐색 호환성을 위한 SameSite=Lax 속성을 지정하고, Access Token의 만료 시간은 짧게 설정해 탈취 시 피해 범위를 줄이겠습니다. Refresh Token은 서버에 별도로 저장해 탈취가 의심되는 경우 즉시 무효화할 수 있도록 하고, 재발급 시마다 새로운 Refresh Token으로 교체하는 로테이션(RTR, Refresh Token Rotation) 방식을 적용해, 이미 사용된 구 토큰으로 재발급이 시도될 경우 탈취로 간주하고 해당 계정의 모든 세션을 즉시 차단하겠습니다. 또한 로그인 및 토큰 재발급 요청에 Rate Limiting을 적용하고, 특정 IP에서 요청이 급증하거나 비정상적인 경로 탐색이 발생하는 경우 이를 탐지해 차단하는 방식으로 보안을 강화하겠습니다.
| 구분 | 내용 |
|---|---|
| 저장 | 쿠키 / HttpOnly |
| 발급절차 | 로그인 시 Access + Refresh 동시 발급 / Access 만료 시 Refresh로 Access만 재발급 |
| 한계 | 탈취 위험, 로그인·재발급 API에 대한 무차별 대입(Brute Force) / DoS성 공격 |
| 극복 ① | SameSite=Lax + Secure로 CSRF 방어 (필요시 Anti-CSRF 토큰 병행) |
| 극복 ② | RTR(Refresh Token Rotation)로 탈취 탐지 및 무효화 |
| 극복 ③ | Rate Limiting으로 API 공격(무차별 대입/DoS) 방어 |