JWT 개념
먼저 JWT에 대해 알아보고, JWT를 구현하는 방법들에 대해 설명하도록 하겠다.
JWT란?
- Json Web Token
- Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token
JWT 구조
- JWT 구성요소
- Header
- Payload
- Signature
각 요소는 모두 JSON 형식을 갖는다.
출처링크: https://doqtqu.tistory.com/277
- Json 형태인 각 부분은 Base64로 인코딩 되어 표현된다.
- 또한 각 요소를 이어 주기 위해, .(마침표) 구분자를 사용하여 구분한다.
- 위 그림의
Encoded
부분이 바로 JWT 토큰 값이다.
Header (헤더)
토큰의 헤더는 typ
과 alg
로 구성된다.
typ
- 토큰의 타입을 지정한다.
- JWT 사용 시,
typ
의 값은 반드시 ‘JWT’ 이다.
alg
- Signature를 해싱하기 위한 알고리즘 방식을 지정한다.
- 즉, Signature의 값은
alg
에 명시된 알고리즘을 사용한 결과이다. - 예시
- HS256
- RSA
어떤 데이터를
alg
알고리즘으로 해싱했는지는 Signature에서 설명함.-
Header 형식 예시
{ "alg": "HS256", "typ": JWT }
PayLoad (페이로드)
페이로드에는 클레임(Claim)들이 담겨 있다.
- 클레임?
- 토큰에서 사용할 정보의 조각
- Key-Value 형태
클레임의 종류는 총 3가지이다.
- 등록된 클레임 (Registered Claim)
- 토큰 정보를 표현하기 위해, 이미 정해진 종류의 데이터
- 즉, 공식적(표준)으로 사용되는 클레임이다.
- 등록된 클레임 종류
iss
: 토큰 발급자 (issuer)sub
: 토큰 제목 (subject), 유니크한 값을 사용한다. (주로 사용자 이메일)aud
: 토큰 대상자 (audience)exp
: 토큰 만료 시간 (expiration), NumericDate 형식nbf
: 토큰 활성 날짜 (not before), 해당 날이 지나기 전의 토큰은 활성화되지 않는다.iat
: 토큰 발급 시간 (issued at), 토큰 발급 이후의 경과시간을 알 수 있다.jti
: JWT 토큰 식별자 (JWT ID), 중복 방지를 위해 사용한다. 일회용 토큰(Access Token) 등에 사용된다.
-
예시
{ "iss": "scope", "sub": "user@gmail.com" }
- 공개 클레임 (Public Claim)
- 사용자 정의 클레임
- 공개용 정보를 위해 사용된다.
- 충돌 방지를 위해, URI 포맷을 이용한다.
-
예시
{ "http://mydomain.com": true }
- 비공개 클레임 (Private Claim)
- 사용자 정의 클레임
- 서버와 클라이언트 사이에 임의로 지정한 정보를 저장한다.
-
예시
{ "myClaim": "myValue" }
Signature (서명)
- Signature는 토큰을 인코딩하거나 유효성 검증을 할 때 사용하는 고유한 암호화 코드이다.
- Signature 생성 과정
- 헤더와 페이로드의 값을 각각 BASE64로 인코딩한다.
- ‘1번에서 인코딩한 값’과 ‘서버 측에 숨겨둔 비밀키’를 Header에서 정의한 알고리즘(
alg
)으로 해싱을 한다. - 2번에서 해싱된 결과를 다시 BASE64로 인코딩한다.
- 3번의 결과가 Signature 이다.
JWT 토큰 예시: 일반적인 경우
출처링크: https://doqtqu.tistory.com/277
- 위 토큰 값은 HTTP 요청 메시지에서
Authorization
이라는 헤더의 value로 사용된다. - 일반적으로 Bearer 라는 단어가 토큰값 앞에 붙는다.
//HTTP 요청 메시지의 헤더
Authorization: Bearer 토큰값
JWT를 구현하는 방법들
방법의 종류
- localStorage에 토큰값을 저장하는 방법
- cookie에 토큰값을 저장하는 방법
- Access 토큰과 Refresh 토큰을 사용하는 방법
1. localStorage에 저장하는 방법
localStorage란?
- 사용자가 key/value 쌍의 형태로 데이터를 유지할 수 있도록하는 간단한 클라이언트 측 데이터베이스이다.
- 로컬 저장소에 데이터를 검색/쓰기하는 매우 간단한 API가 있으며, 도메인당 최대 10MB의 데이터를 저장할 수 있다.
- 쿠키와 달리 저장된 데이터는 모든 HTTP 요청에 포함되지 않는다.
원리
- 클라이언트의 인증(authentication, 로그인) 성공 시, 서버가 JWT 토큰을 응답한다.
- 이때, 단순히 토큰값만을 클라이언트에게 전달한다.
- 클라이언트가 해당 토큰값을 localStorage에 저장한다.
- 클라이언트가 인가(authorization)를 해야할 때마다, localStorage에 저장된 토큰값을 꺼내온다.
-
클라이언트가 HTTP 요청 메시지의 헤더에 아래와 같이 작성한다.
Authorization: Bearer 토큰값
- 서버가 해당 토큰값을 확인하여, 유효한 토큰인지 확인한다.
- 이때
Bearer
문자열은 제외하고, 오직토큰값
만 사용한다. - 토큰의 유효성 검증은 아래와 같다.
- 올바른 JWT 토큰 형식인지?
- 만료시간(
exp
)이 초과되지 않았는지? - 활성 날짜(
nbf
)가 지났는지? - Signature 값이 올바른지?
- 이때
로그아웃 방법
- 클라이언트 측에서, 자신의 localStorage에 저장된 토큰값을 제거한다.
해당 방식의 특징
- CSRF 공격에 안전하다.
- XSS에 취약하다.
- 공격자가 localStorage에 접근하는 JS 코드를 작성하여, 토큰값에 접근할 수 있다.
2. cookie에 저장하는 방법
원리
- 클라이언트의 인증(authentication, 로그인) 성공 시, 서버가 JWT 토큰을 응답한다.
- 이때 서버는 아래와 같이 응답하여, 클라이언트의 쿠키에 토큰 값을 저장한다.
// 자바 코드로 쿠키 설정하는 예시 public void createTokenCookie(HttpServletRequest request, HttpServletResponse response) { Cookie cookie = new Cookie("token", 토큰값); cookie.setHttpOnly(true); //HttpOnly 옵션 설정 cookie.setPath("/"); //경로 설정 response.addCookie(cookie); }
HttpOnly
설정을 통해, JS에서의 쿠키접근을 막는다. (보안성)
- 클라이언트의 쿠키에 해당 토큰값이 저장된다.
- 클라이언트가 요청을 보낼 때마다, 쿠키(
token
)가 함께 전달된다. - 서버가 해당 토큰값을 확인하여, 유효한 토큰인지 확인한다.
- 이때
Bearer
문자열은 제외하고, 오직토큰값
만 사용한다. - 토큰의 유효성 검증은 아래와 같다.
- 올바른 JWT 토큰 형식인지?
- 만료시간(
exp
)이 초과되지 않았는지? - 활성 날짜(
nbf
)가 지났는지? - Signature 값이 올바른지?
- 이때
로그아웃 방법
- 서버 측에서 해당 쿠키를 삭제하도록 응답한다.
해당 방식의 특징
- XSS 공격에 비교적 안전하다.
- 쿠키의
httpOnly
옵션 사용시, js에서 쿠키에 접근할 수 없다. - 하지만 이것만으로 XSS 공격을 완벽히 방어할 순 없다.
- 쿠키의
- CSRF 공격에 취약하다.
3. Access Token과 Refresh Token 을 사용하는 방법
이것이 Best Practice이다.
Access Token과 Refresh Token
- Access Token
- 수명이 보통 몇시간 ~ 몇분이다.
- 해당 토큰으로 인가(Authorization)한다.
- Refresh Token
- 수명이 보통 2주이다.
- 해당 토큰으로 Access Token이 만료되었을 때, 다시 Access Token을 받아온다.
- 요청마다 전달되는 Access Token 보다, 덜 자주 사용되기 때문에 탈취될 위험이 비교적 낮다.
- Access Token이 새로 발급되었을 때, Refresh Token도 새로 발급되도록 하는 것을 IETF 에서 권장하고 있다.
원리
- 클라이언트의 인증(authentication, 로그인) 성공 시, 서버가 ‘Refresh 토큰’과 ‘Access 토큰’을 DB에 1대1로 저장하고 응답한다.
- 클라이언트가 두 토큰을 안전한 곳에 저장한다.
- 클라이언트가 API에 접근할 때마다, Access 토큰을 함께 보낸다.
- 서버가 토큰의 유효성을 검증한다.
- 이때
Bearer
문자열은 제외하고, 오직토큰값
만 사용한다. - 토큰의 유효성 검증은 아래와 같다.
- 올바른 JWT 토큰 형식인지?
- 만료시간(
exp
)이 초과되지 않았는지? - 활성 날짜(
nbf
)가 지났는지? - Signature 값이 올바른지?
- 이때
- 클라이언트가 보낸 Access 토큰이 만료되지 않았으면, 정상적으로 API 응답을 한다.
클라이언트가 보낸 Access 토큰이 만료되었으면, 서버가 토큰이 만료되었다고 응답한다. - 서버로부터 토큰이 만료되었다는 응답을 받으면, 클라이언트는
Access 토큰 재발급 API
을 (Refresh 토큰, 만료된 Access 토큰)과 함께 요청한다. - 서버는 DB에 저장된 Access Token이 실제로 만료되었고, Refresh 토큰이 유효하며, 매핑된 Access 토큰과 Refresh 토큰이 일치한다면, 새로운 Access Token으로 DB를 업데이트하고 응답한다.
만약 해커가 Access 토큰을 탈취했다면?
해커가 Access 토큰을 탈취하더라도, Access 토큰의 유효 시간이 몇 분 ~ 몇 시간 정도이기 때문에 피해를 최소화할 수 있다.
만약 해커가 Refresh 토큰을 탈취했다면?
해커가 Refresh 토큰을 탈취하여, Access 토큰 재발급 API
를 호출한다면 새로운 Access 토큰을 발급받아 악의적인 작업을 할 수 있지 않을까?
결론적으로 그렇지 않다.
- 해커가 탈취한 Refresh 토큰으로 새로운 Access 토큰을 재발급 받고자,
Access 토큰 재발급 API
를 (Refresh 토큰, 임의의 Access 토큰)과 함께 호출한다. - 서버는 해당 Refresh 토큰과 1대1로 매핑된 Access 토큰이 실제로 만료되었고, Refresh 토큰이 유효하며, 매핑된 Access 토큰과 Refresh 토큰이 일치하는지 확인한다. (DB를 통해)
- 하지만 Access 토큰이 실제로 만료되지 않았거나 Refresh 토큰과 매핑되지 않기 때문에, 비정상적인 접근으로 판단하여 해당 Refresh 토큰과 Access 토큰을 모두 제거한다.
- 결과적으로 해커는 접근할 수 없다.
정상 사용자도 다시 로그인을 해야하지만 안전하다.
그렇다면 운좋게 해커가 Access 토큰이 만료된 시점에 Access 토큰 재발급 API
를 호출하면 어떻게 될까?
- 해커가 탈취한 Refresh 토큰으로 새로운 Access 토큰을 재발급 받고자,
Access 토큰 재발급 API
를 (Refresh 토큰, 임의의 Access 토큰)과 함께 호출한다. - 서버는 해당 Refresh 토큰과 1대1로 매핑된 Access 토큰이 실제로 만료되었고, Refresh 토큰이 유효하며, 매핑된 Access 토큰과 Refresh 토큰이 일치하는지 확인한다. (DB를 통해)
- 실제로 Access 토큰이 만료되었더라도, Refresh 토큰과 매핑되지 않기 때문에 비정상 접근으로 처리한다.
- 결과적으로 해커와 정상 사용자 모두 다시 로그인해야한다.
해커가 Access Token과 Refresh Token 모두 탈취했다면?
이 경우, 대응하기 매우 어렵다. 따라서 최대한 Token이 노출되는 것을 방지해야 한다.
로그아웃 방법
- Refresh 토큰이 담긴 쿠키를 제거하도록, 서버가 응답한다.
해당 방식의 특징
- XSS 공격을 방어할 수 있다.
- CSRF 공격을 방어할 수 있다.