혹시 아직도 사용자의 액세스 토큰(Access Token)을 브라우저의 LocalStorage에 저장하고 계신가요? 만약 그렇다면, 당신의 서비스는 XSS(교차 사이트 스크립팅) 공격 한 번에 모든 사용자 계정을 탈취당할 위기에 처해 있습니다. "남들도 다 그렇게 하던데?"라는 안일한 생각은 보안 사고의 시작입니다.
MSA(마이크로서비스)와 SPA(싱글 페이지 애플리케이션) 시대의 표준이 된 JWT(JSON Web Token). 하지만 단순히 라이브러리를 가져다 쓰는 것과 '안전하게 설계'하는 것은 완전히 다른 차원의 문제입니다. 이 글에서는 JWT의 해부학적 구조부터, 시니어 엔지니어들이 현업에서 적용하는 '리프레시 토큰 로테이션' 전략까지, 당신의 인증 서버를 철통같이 지키는 방법을 공개합니다.
1. JWT 구조: 인코딩은 암호화가 아니다
많은 개발자가 오해하는 첫 번째 사실은 "JWT는 암호화되어 있어 안전하다"는 착각입니다. JWT는 서명(Signed)된 것이지 암호화(Encrypted)된 것이 아닙니다. 누구나 Base64Url 디코딩만 하면 페이로드의 내용을 100% 열람할 수 있습니다.
JWT는 점(.)으로 구분된 세 부분으로 나뉩니다:
- Header: 알고리즘과 토큰 타입을 정의 (예:
{"alg": "HS256", "typ": "JWT"}) - Payload: 실제 데이터(Claim)가 들어가는 곳 (예:
{"sub": "user123", "role": "admin"}) - Signature: 데이터 무결성을 보장하는 핵심 서명 값
2. 상태 비저장(Stateless)의 양면성
서버가 세션을 기억하지 않아도 된다는 'Stateless' 특성은 서버 확장(Scale-out)에 엄청난 이점을 줍니다. 하지만 여기에는 치명적인 단점이 숨어 있습니다. 바로 "이미 발급된 토큰은 서버가 뺏을 수 없다"는 것입니다.
사용자가 로그아웃을 하거나 관리자가 악성 유저를 차단하려 해도, 해당 유저가 가진 JWT의 만료 시간(exp)이 남아있다면 그 토큰은 여전히 유효합니다. 이를 해결하기 위해 '블랙리스트'를 운영하기도 하지만, 이는 결국 Redis 같은 저장소를 필요로 하여 Stateless의 장점을 희석시킵니다.
3. 철옹성 아키텍처: Access & Refresh Token 전략
가장 안전하고 현실적인 아키텍처는 수명이 짧은 Access Token과 수명이 긴 Refresh Token을 함께 사용하는 것입니다. 이 구조의 핵심은 토큰의 '저장 위치'에 있습니다.
| 토큰 종류 | 유효 기간 | 저장 위치 (권장) | 역할 |
|---|---|---|---|
| Access Token | 30분 이내 | 메모리 (변수) | API 요청 시 인증용 |
| Refresh Token | 2주 이상 | HttpOnly Cookie | Access Token 재발급용 |
왜 HttpOnly Cookie인가?
자바스크립트로 접근 가능한 LocalStorage나 SessionStorage는 XSS 공격에 취약합니다. 반면, HttpOnly 속성이 설정된 쿠키는 브라우저의 자바스크립트 엔진이 접근할 수 없습니다. 즉, 해커가 악성 스크립트를 심어도 리프레시 토큰을 훔쳐갈 수 없습니다.
# Python (FastAPI) 예시: 안전한 쿠키 설정 방법
from fastapi import Response
def set_refresh_token(response: Response, refresh_token: str):
response.set_cookie(
key="refresh_token",
value=refresh_token,
httponly=True, # JS 접근 불가 (XSS 방지)
secure=True, # HTTPS에서만 전송
samesite="Lax", # CSRF 완화
max_age=60 * 60 * 24 * 14 # 14일
)
4. 구현 시 절대 저지르면 안 되는 실수들
구조를 잘 잡아도 디테일에서 뚫릴 수 있습니다. 다음 체크리스트를 반드시 확인하십시오.
- 알고리즘 검증 부재 (Alg: None Attack): 해커가 헤더의
alg를none으로 바꾸고 서명을 지워서 보내는 공격입니다. 서버는 반드시 허용된 알고리즘(예:HS256)인지 명시적으로 확인해야 합니다. - 취약한 비밀키 사용: 비밀키(Secret Key)는 무차별 대입 공격(Brute Force)에 대비해 충분히 길고 복잡해야 합니다. 단순한 단어나 짧은 문자열은 몇 시간 만에 뚫릴 수 있습니다.
- 예외 처리 미흡:
ExpiredSignatureError(만료됨)와InvalidTokenError(위조됨)를 구분하여 처리해야 합니다. 만료된 경우에는 재발급 로직으로 유도하고, 위조된 경우에는 보안 경고를 띄워야 합니다.
결론 및 요약
JWT는 강력한 도구이지만, 잘못 사용하면 '보안 구멍'이 됩니다. 핵심은 Access Token의 수명을 짧게 유지하고, Refresh Token을 HttpOnly 쿠키로 보호하며, 필요시 RTR 기법을 적용하여 탈취 시나리오를 무력화하는 것입니다. 편의성을 위해 보안을 희생하지 마십시오. 오늘 당장 여러분의 인증 로직을 점검하고 안전한 아키텍처로 전환하시기 바랍니다.
Post a Comment