토큰 인증 패턴 가이드

시나리오 00 · 배경

쿠키 세션 방식은
어디서 무너졌나

액세스/리프레시 토큰 체계 이전의 주류는 쿠키 기반 세션(Session Cookie)이었습니다. 로그인하면 서버가 세션 ID를 만들어 DB에 저장하고, 그 ID를 쿠키로 브라우저에 보냅니다. 이후 모든 요청마다 서버는 DB에서 세션을 조회해 인증합니다. 작동은 했지만, 세션 ID 탈취 시 피해 제어서버 확장 두 곳에서 한계가 드러났습니다.

🕰️
핵심 포인트
쿠키 세션의 두 가지 구조적 문제:
세션 ID 탈취 = 만료까지 무제한 사칭 — 서버는 올바른 ID를 가진 요청을 무조건 통과시킵니다.
서버가 모든 세션을 DB에 보관 — 서버를 여러 대로 늘릴 때(Scale-out) 세션 공유가 복잡해집니다.

액세스/리프레시 토큰 분리는 ①을 해결합니다: 액세스 토큰을 15분으로 짧게 두면 탈취되어도 피해 기간이 제한됩니다. 리프레시 토큰은 서버 DB에 기록해 의심 시 즉시 무효화할 수 있습니다.
과거
쿠키 세션 방식
로그인 → 서버가 세션 ID 생성·DB 저장 → 세션 ID를 쿠키로 전달 → 이후 모든 요청마다 DB 조회로 인증.
문제1
세션 ID 탈취 → 만료까지 무제한 사칭
세션 ID가 탈취되면 공격자는 그 ID로 계속 요청을 보낼 수 있습니다. 서버는 올바른 ID가 오는 한 누가 보내는지 알 수 없습니다.
문제2
서버 확장이 어려움
세션 데이터가 서버 메모리나 DB에 묶여 있어, 서버를 여러 대로 늘릴 때 세션 공유 문제가 생깁니다.
개선
액세스 + 리프레시 토큰으로 분리
짧은 수명의 액세스 토큰으로 피해 기간을 제한하고, 리프레시 토큰을 서버에서 관리해 즉시 무효화가 가능해집니다.
다이어그램 렌더링 중…
시나리오 01 · 정상 흐름

로그인하고
API를 호출하기까지

사용자가 로그인하면 서버는 두 가지 토큰을 발급합니다. 액세스 토큰은 "지금 당장 쓸 수 있는 출입증"이고, 리프레시 토큰은 "출입증이 만료됐을 때 새 출입증을 받는 재발급권"입니다. 출입증은 짧게, 재발급권은 길게 유효하게 설정합니다.

💡
핵심 포인트
액세스 토큰은 15분처럼 짧게 설정합니다. 누군가 훔쳐도 곧 만료되어 쓸 수 없게 되기 때문입니다. 리프레시 토큰은 7일~30일로 길게 설정하되, 서버 DB에 기록해서 필요하면 언제든 무효화할 수 있습니다. 이 두 역할을 분리한 덕분에 보안과 편의성을 동시에 얻을 수 있습니다.

저장 방식: 액세스 토큰은 JS 메모리에, 리프레시 토큰은 httpOnly 쿠키에 저장합니다. httpOnly 쿠키는 자바스크립트에서 직접 읽을 수 없어 XSS 공격에 안전하고, 브라우저가 요청 시 자동으로 함께 전송합니다.
01
사용자 로그인 요청
이메일/비밀번호를 서버로 전송합니다.
02
서버가 두 가지 토큰 발급
액세스 토큰(15분)은 응답 바디로, 리프레시 토큰(7일)은 httpOnly 쿠키로 전달합니다.
03
클라이언트에 저장
액세스 토큰은 JS 메모리에 보관. 리프레시 토큰은 브라우저가 httpOnly 쿠키로 자동 관리 (JS로 직접 접근 불가).
04
API 호출 시 액세스 토큰 첨부
Authorization: Bearer {액세스토큰} 헤더로 요청합니다.
다이어그램 렌더링 중…
시나리오 02 · 자동 갱신

액세스 토큰이
만료됐을 때

15분이 지나 액세스 토큰이 만료되면 서버는 401 오류를 반환합니다. 클라이언트는 사용자 모르게 리프레시 토큰을 이용해 새 액세스 토큰을 받고, 원래 요청을 다시 보냅니다. 사용자는 아무것도 느끼지 못합니다.

🔄
핵심 포인트
이 과정을 자동 갱신(Silent Refresh — 사용자 눈에 띄지 않게 새 토큰을 받는 과정)이라고 합니다. 클라이언트는 서버에서 401(출입증이 더 이상 유효하지 않다는 신호)을 받으면, 자동으로 재발급권(리프레시 토큰)을 써서 새 출입증을 받아온 뒤 원래 요청을 다시 시도합니다. 사용자는 아무것도 느끼지 못하고 앱이 끊김 없이 동작합니다.
01
API 요청 → 401 응답
만료된 액세스 토큰으로 요청했더니 서버가 401을 돌려줍니다.
02
리프레시 토큰으로 갱신 요청
POST /auth/refresh를 호출합니다. 리프레시 토큰은 httpOnly 쿠키로 자동 전송됩니다.
03
새 액세스 토큰 수신 및 저장
서버가 새 액세스 토큰(15분)을 발급합니다.
04
원래 API 재요청 → 성공
새 토큰으로 원래 요청을 다시 보내 정상 응답을 받습니다.
다이어그램 렌더링 중…
시나리오 03 · 재로그인

리프레시 토큰도
만료됐을 때

리프레시 토큰도 만료(또는 무효화)되면 더 이상 갱신이 불가능합니다. 서버는 두 번째 401을 반환하고, 클라이언트는 모든 토큰을 삭제한 뒤 사용자를 로그인 화면으로 이동시킵니다.

⚠️
핵심 포인트
리프레시 토큰 갱신 요청에서도 401이 오면 "세션 완전 만료"로 처리합니다. JS 메모리의 액세스 토큰을 삭제하고, 리프레시 토큰 쿠키는 서버가 만료된 Set-Cookie를 내려보내 제거합니다 (httpOnly 쿠키는 JS가 직접 삭제할 수 없습니다). 서버가 강제 로그아웃(토큰 무효화)을 할 때도 이 경로로 처리됩니다.
01
API 요청 → 401 (액세스 토큰 만료)
자동 갱신을 시도합니다.
02
갱신 요청 → 401 (리프레시 토큰도 만료)
서버가 리프레시 토큰도 거부합니다.
03
클라이언트: 액세스 토큰 삭제 + 쿠키 제거 요청
JS 메모리의 액세스 토큰을 삭제합니다. 리프레시 토큰(httpOnly 쿠키)은 JS로 직접 지울 수 없으므로, 서버의 401 응답에 만료된 Set-Cookie가 포함되어 있어야 브라우저가 쿠키를 삭제합니다.
04
로그인 페이지로 리다이렉트
사용자에게 "세션이 만료되었습니다" 안내 후 로그인을 요청합니다.
다이어그램 렌더링 중…
시나리오 04 · 로그아웃

안전하게
로그아웃하기

로그아웃은 단순히 토큰을 삭제하는 것이 아닙니다. 서버에 리프레시 토큰을 알려주어 DB에서 무효화해야 탈취된 리프레시 토큰으로 새 액세스 토큰을 발급받는 것을 막을 수 있습니다.

🔒
핵심 포인트
클라이언트에서 토큰을 지우는 것만으로는 부족합니다. 다른 기기나 공격자가 리프레시 토큰을 갖고 있다면 새 출입증을 계속 만들 수 있기 때문입니다. 서버에 로그아웃을 알려 해당 리프레시 토큰을 DB에서 삭제(무효화)해야 완전한 로그아웃이 됩니다. 리프레시 토큰은 httpOnly 쿠키로 자동 전송되므로, 클라이언트가 별도로 읽어서 보낼 필요가 없습니다.
01
사용자가 로그아웃 버튼 클릭
클라이언트가 로그아웃 요청을 준비합니다.
02
서버에 로그아웃 요청
POST /auth/logout을 호출합니다. 리프레시 토큰은 httpOnly 쿠키로 자동 전송됩니다.
03
서버: 리프레시 토큰 무효화
DB에서 해당 토큰을 삭제하거나 블랙리스트에 등록합니다.
04
클라이언트: 액세스 토큰 삭제 + 쿠키 제거
JS 메모리의 액세스 토큰을 삭제합니다. 서버가 응답에 만료된 Set-Cookie를 포함해 브라우저의 리프레시 토큰 쿠키를 제거합니다. 로그인 화면으로 이동합니다.
다이어그램 렌더링 중…
시나리오 05 · 토큰 로테이션

리프레시 토큰도
교체하기

리프레시 토큰을 쓸 때마다 새 것으로 교체하는 방식을 토큰 로테이션(Rotation — 갱신할 때마다 재발급권도 새것으로 바꾸는 방식)이라고 합니다. 서버는 이전 재발급권을 "사용됨"으로 기록해 두고, 누군가 그것을 다시 가져오면 탈취로 판단할 수 있습니다. 보안이 중요한 서비스에서 널리 쓰이는 패턴입니다.

🔁
핵심 포인트
갱신 요청 시 서버는 새 액세스 토큰 + 새 리프레시 토큰을 함께 발급합니다. 이전 리프레시 토큰은 단순히 삭제하는 것이 아니라 "사용됨(revoked)" 상태로 DB에 기록합니다. 그래야 나중에 같은 토큰이 다시 들어왔을 때 "이미 썼던 재발급권을 또 쓰려 한다"는 것을 감지할 수 있습니다.
01
액세스 토큰 만료 → 갱신 요청
기존 리프레시 토큰(RT-A)이 쿠키로 자동 전송됩니다.
02
서버: 새 토큰 쌍 발급 + RT-A 상태 기록
새 액세스 토큰(AT-2) + 새 리프레시 토큰(RT-B)을 발급하고, RT-A는 DB에서 "사용됨(revoked)"으로 표시합니다.
03
클라이언트: 두 토큰 모두 교체 저장
새 액세스 토큰은 메모리에, 새 리프레시 토큰 RT-B는 쿠키로 업데이트됩니다.
04
다음 갱신은 RT-B로
RT-A는 이미 "사용됨" — 누군가 다시 사용하면 서버가 탈취로 감지합니다.
다이어그램 렌더링 중…
시나리오 06 · 보안

탈취된 토큰
재사용 공격 감지

예를 들어 사용자의 기기가 해킹되거나 네트워크가 탈취되어 공격자가 리프레시 토큰을 손에 넣었다고 가정합니다. 공격자가 그 토큰으로 먼저 갱신 요청을 보내면, 정상 사용자가 뒤이어 갱신할 때 이미 "사용됨" 처리된 토큰을 보내게 됩니다. 서버는 이를 재사용 공격(Reuse Attack)으로 판단해 이 로그인에서 발급된 모든 토큰을 한번에 무효화합니다.

🚨
핵심 포인트
이미 "사용됨(revoked)"으로 기록된 리프레시 토큰이 다시 들어오면, 서버는 "이 재발급권이 다른 곳에서 이미 쓰였다 → 탈취됐을 가능성이 높다"고 판단합니다. 이때 해당 로그인 계보(패밀리 — 한 번의 로그인에서 이어져 나온 토큰들의 계보) 전체를 무효화해서 공격자와 정상 사용자 모두 로그아웃시킵니다.

단, 공격자가 이미 발급받은 액세스 토큰은 만료 전까지 일시적으로 유효할 수 있습니다. 이를 즉시 차단하려면 서버가 모든 요청에서 세션 상태를 추가로 검사해야 합니다. 그래서 액세스 토큰 유효 시간을 짧게 설정하는 것이 중요합니다.
01
공격자: 탈취한 RT-A로 먼저 갱신 성공
공격자가 RT-A로 갱신해 새 리프레시 토큰(RT-B)과 액세스 토큰(AT-2)을 획득합니다. 서버는 RT-A를 "사용됨"으로 기록합니다.
02
정상 사용자: 같은 RT-A로 갱신 시도 → 이상 감지
사용자 입장에선 "왜 갑자기 토큰 갱신이 안 되지?"라는 상황. 서버는 이미 "사용됨"인 RT-A가 또 들어온 것을 확인합니다.
03
서버: 재사용 감지 → 패밀리 전체 무효화
RT-A에서 파생된 RT-B 등 이 로그인 계보의 모든 리프레시 토큰을 DB에서 삭제합니다.
04
공격자 + 정상 사용자 모두 재로그인 필요
공격자의 RT-B도 무효화. 공격자가 가진 AT-2는 짧은 만료 시간 후 자연 소멸됩니다.
다이어그램 렌더링 중…