이전 User 관련 클래스 생성에 이어서 다음에는 JWT 관련 클래스를 생성해보려고 합니다.
JWT 관련 클래스를 생성하기 전에, JWT가 무엇인지 간단하게 살펴보고자 합니다.
1. JWT(JSON Web Token)란?
JWT는 인증에 필요한 정보들을 암호화 시킨 JSON 토큰을 의미합니다.
따라서 JWT를 이용한 인증은 유저를 인증하고 식별하기 위한 Token 기반 인증입니다.
JWT는 JSON 데이터를 Base64 URL-safe Encode를 통해 인코딩하여 직렬화한 것이고,
토큰 내부에는 개인키를 통한 전자서명이 들어있습니다.
※ Base64 URL-safe Encode란, 일반적인 Base64 Encode를 URL에서 오류 없이 사용하도록
'+'와 '/'를 각각 '-', '_'으로 표현한 것입니다.
토큰 기반 인증에서 토큰은
토큰 자체에 사용자의 정보들이 포함되어 있다는 점(Self-contained)이 특징입니다.
클라이언트의 상태를 알아야했던 Stateful했던 환경에서 JWT를 사용하게 되면
서버가 클라이언트의 상태를 저장하지 않아도 되기 때문에 Stateless하게 설계가 가능합니다.
JWT를 통한 인증은 다음과 같은 순서로 진행됩니다.
1. 사용자가 로그인 시 로그인 아이디, 패스워드를 담아 서버에 요청
2. 서버에서 서명된(Signed) JWT 토큰을 생성하여 클라이언트에 응답으로 반환
3. 클라이언트는 응답으로 반환된 JWT 토큰을 사용하여 요청 시 마다 Http Header에 JWT를 담아 요청
4. 서버에서는 요청된 Http Header의 JWT를 검증하여 토큰이 유효한지 검증 후 유효하다면 요청에 맞는 응답 반환
2. JWT의 구조
JWT는 각각의 구성 요소가 .으로 구분되어 있으며, 구성 요소는 다음 세 가지로 구성됩니다.
- Header
- Payload
- Signature
1. Header
header에는 보통 토큰의 타입이나, 전자서명 시 어떤 알고리즘이 사용되었는지 저장합니다.
현재 프로젝트에 적용한 JWT에서는 HS512 알고리즘을 사용하고 있습니다.
2. Payload
Payload에는 보통 Claim이라는 토큰에서 사용할 정보들이 담겨있습니다.
위의 PAYLOAD에서 key-value 형식으로 이루어진 하나의 쌍들이 모두 Claim입니다.
인증 시에 토큰에서 실제로 사용될 정보를 의미합니다.
여러 Claim들을 JWT 토큰 생성 시에 개발자가 어떤 Claim을 넣을지 정한 후 마음대로 넣을 수 있습니다.
JWT의 표준 스펙에는 7가지의 Claim이 정의되어 있습니다.
표준 스펙에 정의된 것 뿐이지, 꼭 7가지를 모두 포함해야 되는 것은 아니며,
실제 위의 JWT Payload에서도 표준 스펙 중에서는 2가지를 포함하고 있습니다.
1. iss(Issuer) : 토큰 발급자
2. sub(Subjec) : 토큰 제목 - 토큰에서 사용자에 대한 식별값이 된다.
3. aud(Audience) : 토큰 대상자
4. exp(Expiration Time) : 토큰 만료 시간
5. nbf(Not Before) : 토큰 활성 날짜 (이 날짜 이전의 토큰은 활성화 되지 않음을 보장)
6. iat(Issued At) : 토큰 발급 시간
7. jti(JWT Id) : JWT 토큰 식별자 (issuer가 여러 명일 때 구분하기 위한 값)
이렇게 표준 스펙에 7가지 Claim이 정의되어 있고,
필요하다면 개발자가 추가로 작성해도 문제가 없습니다.
실제 저의 프로젝트에서도 토큰에서 사용자의 이메일을 추출하기 위해,
위의 JWT Payload에서 3번째 Claim으로 사용자 정의 Claim인 'email'을 별도로 추가했습니다.
✅ 이때, 주의해야 할 점은 Payload에는 암호화가 되어 있지 않기 때문에, 민감한 정보를 담지 않아야 합니다.
누구나 JWT Decoding을 통해 Payload의 정보를 볼 수 있기 때문에 민감한 정보를 넣지 말고,
단순하게 "식별을 위한" 정보만을 담아두어야 합니다.
3. Signature
JWT 구조에서 가장 중요한 Signature(서명)입니다.
JWT Signature는 암호화되어 있기 때문에, 외부에서 위의 사진처럼 Decoding을 진행해도
실제 서명부가 나오지 않고, 암호화의 구조만 나타나게 됩니다.
암호화 구조를 살펴보면, 앞서 JWT 정의에 대해서 말할 때 언급되었던
base64UrlEncode를 사용하여 header와 payload를 암호화한 것을 볼 수 있습니다.
그 다음은 your-256-bit-secret로, 서버가 가지고 있는 개인키를 통해 암호화되어 있습니다.
이렇게 서버가 가지고 있는 개인키를 통해 암호화되어 있기 때문에
외부에서 Signature를 복호화할 수 없는 것입니다.
JWT 구조를 통해 이해하는 JWT 인증 과정
1. JWT 토큰을 클라이언트가 서버에 요청 시 Http Header에 담아 요청합니다.
2. 서버에서 Http Header의 JWT 토큰을 꺼내서 가져옵니다.
3. 클라이언트가 요청한 JWT 토큰을 서버가 가지고 있는 개인키를 가지고 Signature를 복호화합니다.
4. 복호화한 Signature의 base64UrlEncode(header)/base64UrlEncode(payload)가
각각 요청한 JWT 토큰의 header, payload와 일치하는지 검증합니다.
5. 일치한다면 인증을 허용하고, 일치하지 않는다면 인증이 실패합니다.
👉 이 과정에서 만약 해커가 header나 payload의 값을 변조한 상태로 서버에게 요청을 보내게 된다면,
서버 JWT 검증 단계에서 Signature를 복호화했을 때의 header나 payload 값과 다르기 때문에
서버에서 인증이 실패되었다고 Response를 보낼 것입니다.
3. JWT - AccessToken & RefreshToken
보통 JWT라고 하면, 인증 시 사용되는 AccessToken을 의미하게 됩니다.
그렇다면, 'RefreshToken'이라고 하는 토큰 개념은 왜 생겨나게 되었는지 이해해봅시다.
모든 것은 탈취 위험에서부터 시작합니다.
만약, 해커가 JWT AccessToken을 탈취한다면?
해커는 탈취한 AccessToken을 사용하여 접근이 모두 가능해질 것입니다.
이를 해결하기 위해, AccessToken의 유효 기간을 짧게 하면 해결할 수 있습니다.
만약 해커가 탈취를 하더라도, 그 AccessToken을 짧은 시간동안 밖에 못 쓰기 때문에 대응이 되는 것입니다.
하지만, AccessToken의 유효 기간을 짧게 설정하게 되면 유저 입장에서 매우 귀찮고 번거로울 것입니다.
로그인 한지 얼마 되지도 않아 다른 페이지를 이동할 때
다시 로그인을 해서 AccessToken을 발급 받아야하기 때문입니다.
따라서, 해커 탈취 문제-사용자의 이용성에 trade-off가 발생하게 되는 것입니다.
이러한 trade-off를 해결해 주는 것이 바로 RefreshToken입니다.
RefreshToken은 인증이 아닌, AccessToken을 재발급 해주는 역할의 Token입니다.
따라서, RefreshToken만 가지고는 인증을 성공할 수 없습니다.
AccessToken과 RefreshToken 모두 JWT이지만, 서로 역할이 다릅니다.
1. AccessToken
처음 로그인 요청 시 서버에서 실제 유저의 정보가 담긴 AccessToken을 발행합니다.
클라이언트는 이 AccessToken을 저장한 후, 요청마다 AccessToken을 보내서
해당 AccessToken을 서버에서 검증 후 유효하면 요청에 맞는 응답을 진행합니다.
2. RefreshToken
처음 로그인 요청 시 서버에서 AccessToken 재발급 용도인 RefreshToken을 발행합니다.
이때, 클라이언트는 RefreshToken을 저장하지 않고 RefreshToken은 보통 서버 DB에 저장됩니다.
RefreshToken이 유효하면, AccessToken의 재발급을 진행합니다.
요약하면, AccessToken은 인증 처리 역할 / RefreshToken은 AccessToken 재발급 역할을 수행합니다.
그렇다면, 구체적으로 어떻게 RefreshToken이
해커 탈취 문제-사용자의 이용성 trade-off를 해결할 수 있는지 살펴봅시다.
먼저, 해커 탈취 문제 대응을 위해 AccessToken의 유효 기간을 1일에서 1시간으로 변경했다고 해봅시다.
만약, RefreshToken이 없다면 위에서 언급했던 것처럼 사용자는 1시간마다 로그인을 해줘야할 것입니다.
여기서, RefreshToken은 유효 기간이 7일이라고 해봅시다.
AccessToken이 1시간이 지나 만료 후 클라이언트가 요청을 보낼 때,
RefreshToken 로직이 추가되면, 서버에서는 인증 실패가 아닌 RefreshToken 검증 단계에 진입합니다.
RefreshToken이 유효하다면, 그 즉시 클라이언트에게 새로운 AccessToken을 발행해주고,
클라이언트는 그 AccessToken을 받아 재요청을 하게 됩니다.
따라서, 사용자의 눈에는 별도의 재로그인 과정없이 AccessToken이 만료되지 않은 것처럼 동작하게 됩니다.
RefreshToken의 유효 기간이 7일이기 때문에,
결국 사용자는 AccessToken의 유효 기간이 7일인 것처럼 사용이 가능한 것입니다.
물론 RefreshToken도 해커에게 탈취되면 AccessToken을 해커가 재발급 받을 수 있기 때문에 위험하지만,
RefreshToken은 클라이언트에 저장되는 것이 아닌 서버 DB에 저장되기 때문에, 해커 탈취 위험이 적습니다.
이렇게 해커 탈취 문제-사용자의 이용성 trade-off를 해결하는 것입니다.
더 자세한 부분은 아래의 블로그를 참고하면 좋을 것 같습니다!
📖 깃허브 링크 (전체 코드)
https://github.com/KSH-beginner/oauth2WithJwtLogin
📕 전체 로그인 구현 목차
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (1) - 회원(User) 관련 클래스 생성
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (2) - JWT란?
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (3) - JWT 관련 클래스 생성 / JWT 인증 로직
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (4) - 자체 JSON 로그인 커스텀하기
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (5) - OAuth란? / OAuth 2.0 인증 과정 예시
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (6) - OAuth 2.0 로그인 구현 사전 설정
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (7) - OAuth 2.0 로그인 관련 클래스 생성
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (8) - SecurityConfig 설정 클래스 생성
Spring Security + JWT를 이용한 자체 Login & OAuth2 Login API 구현 (9)- JWT 자체 로그인 & OAuth2 Login 테스트
Reference
https://brunch.co.kr/@jinyoungchoi95/1