JWT란?
- JSON Web Token의 약자
- 일반적으로 사용자 인증을 위해 사용
- Base64 URL Safe 방식으로 인코딩되어 있기 때문에 누구나 읽을 수 있으므로, 중요한 정보를 입력하지 않도록 주의
- 기본적으로 상태가 없지만(Stateless), 인증과 관련된 경우 보안상의 이유로 대부분 상태를 갖도록(Stateful) 구현됨
JWT 구성
- Header, Payload, Signature 세 파트로 나뉘어지는데 각각
.
으로 구분 - 각 파트는 Base64 URL Safe 방식으로 인코딩 되어있음
Header
- 헤더에는 타입과 해시 알고리즘 정보 2가지가 저장됨
- 타입: JWT
- 알고리즘: HMAC SHA256(HS256) 또는 RSA
{ "typ": "JWT", "alg": "HS256" }
Payload
- claim
- JWT에서 claim은 key-value 쌍을 의미
- 여러 claim을 저장하고 있음
- 주로 사용자의 상태가 저장됨
- claim type
- Registered claims
- 공통적으로 사용하기에 유용하므로 추천되는 claim 목록
- iss (issuer), exp (expiration time), sub (subject), aud (audience), nbf (not before), iat (issued at), jti (jwt id)
- Public claims
- 자유롭게 이름을 정해서 사용할 수 있음(이름 충돌 방지 필요)
- 이곳에 정의되어 있는 이름을 사용해서 충돌을 방지하거나 UUID 등을 사용해서 충돌 방지
- 충돌 방지된 이름을 사용하면 Public claim
- Private claims
- 자유롭게 이름을 정해서 사용할 수 있음(이름 충돌 방지 필요 없음)
- 다른 claim과 이름이 겹치지 않도록 주의해서 사용
- 충돌 방지된 이름을 사용하지 않으면 Private claim
{ "sub": "1234567890",// Registered claim "name": "John Doe",// Public claim "admin": true// Private claim }
- Registered claims
Signature
- Header에 정의된 알고리즘으로 Header, Payload, Secret Key를 조합해서 생성
- JWT 검증을 위해 사용
- Secret Key는 유출되지 않도록 검증 주체(서버)만 보관하고 있어야 함
JWT 사용법
1. [클라이언트 → 서버] 사용자 인증(로그인)
클라이언트(웹)
- 서버: 응답
- Access token: Response Body로 응답
- Refresh token: Cookie로 응답
- 쿠키 옵션
HttpOnly
: JavaScript가 Cookie에 접근하는 것을 제한하여 XSS 방지Secure
: 보안 채널(일반적으로 HTTPS 프로토콜)인 경우만 요청에 Cookie를 포함하여 XSS 방지
- 쿠키 옵션
- 클라이언트: 안전하게 보관
- Access token: 메모리(private 변수처럼 탈취 위험이 낮은 곳)에 저장하여 XSS와 CSRF 방지
- 쿠키에 저장되지 않으므로 CSRF 공격 불가능
- 탈취 위험이 낮은 private 변수에 저장하여 XSS 방지
- (클라이언트가 SPA가 아닌 경우) 페이지 이동 시 토큰이 사라지기 때문에, 모든 요청마다 Refresh token을 통해 Access token 재발급 필요
- Refresh token: Cookie에 저장
- CSRF 공격을 통해 Cookie에 저장된 Refresh token으로 Access token을 재발급 받더라도, 공격자는 Access token을 알 수 없으므로 악의적 요청 불가능
- Access token: 메모리(private 변수처럼 탈취 위험이 낮은 곳)에 저장하여 XSS와 CSRF 방지
클라이언트(모바일 앱)
- 서버: 응답
- Access token: Response Body로 응답
- Refresh token: Response Body로 응답
- 클라이언트: 안전하게 보관
- iOS Keychain 이나 Android EncryptedSharedPreferences 같은 보안 저장소에 보관
2. [클라이언트 → 서버] 사용자 인증이 필요한 요청
- 클라이언트: Access token과 함께 요청
- Request header의 Authorization 필드에 Bearer 방식으로 Access token 세팅
- Request body와 함께 요청
- 서버: 검증 후 요청 수행
- Access token 검증
- 검증 통과 시 요청한 로직 수행
Stateless하게 구성한 JWT
검증
- Secret key로 토큰 signature 검증
- 토큰 만료 시간 검증
장점
- JWT 검증만 하면 되므로 트래픽 부담 낮음(DB 조회 X)
- Stateful(세션 기반 인증)인 경우, 요청마다 세션 정보를 저장하고 있는 메모리 DB에서 클라이언트 정보를 확인해야 함
단점
- 토큰이 탈취되면 서버에서 토큰을 폐기시켜야 하는데, 세션은 초기화 시키면 되지만 JWT는 이런 기능을 제공하지 않는다
Stateful하게 구성한 JWT(토큰 관리 및 보안 강화)
검증
- Secret key로 토큰 signature 검증
- 토큰 만료 시간 검증
- 블랙리스트에 포함된 토큰인지 조회
토큰 탈취(비정상적인 요청) 탐지
- 비정상적인 요청의 기준은 웹 서버마다 다르다
- 예시
- IP 주소, User-Agent: 이전과 다른 위치나 기기에서 요청이 발생한 경우
- 동시 로그인: Refresh token 또는 Access token이 여러 다른 기기에서 사용되는 경우
- 비정상적인 활동 패턴: Refresh Token이나 Access Token이 지나치게 자주 사용되는 경우
- 예시
토큰 탈취 해결책
- 서버에서 토큰을 블랙리스트 DB에 추가
장점
- Stateful하게 DB를 사용하면 토큰 관리 가능
단점
- JWT의 장점은 무상태성(Stateless)인데, 보안을 위해 Stateful하게 구성하면 결국 무상태성이라는 장점이 없어진다
- DB 조회 필요
세션/쿠키 방식과 JWT 비교
세션/쿠키 | JWT | |
클라이언트 | 세션 ID 저장 | Access token, Refresh token 저장 |
Scale-out | 분산 환경에서 redis로 세션 공유하여 관리 | 분산 환경에서 DB로 토큰 블랙리스트 공유하여 관리(보안) |
캐시/DB 조회 | O | O |
CSRF 토큰 | O | X |
HTTP 요청 크기 | 상대적으로 적다 - 세션 ID - CSRF 토큰 |
상대적으로 크다 - JWT(필드 추가될수록 토큰 길어짐) |
결론
- 보안을 위해 JWT를 Stateful로 구성한다면 세션/쿠키 방식과 큰 차이가 없다
- 일회성 인증 등 상태를 유지할 필요 없는 경우는 JWT가 좋은 선택이 될 수 있다