-
[Spring boot] JWT(JSON Web Token)프로젝트/Share Your Trip 2024. 1. 12. 22:46
JWT(JSON Web Token) 왜 궁금했을까 ❓
- NodeJS를 이용해서 JWT 인증 방식을 구현해 본 경험이 있지만, Spring boot로 JWT(JSON Web Token) 인증을 구현해본 경험이 없어서 시간이 부족했던 1학기 프로젝트에서 JWT를 적용하지 못했다.
- 개발 시간이 비교적 널널한 2학기 프로젝트에서 JWT 인증을 도입하여 OAuth 확장성과 낮은 성능의 서버로 인해 Client에서 인증 정보를 관리하도록 해보려고 한다.JWT를 사용해야 하는 이유 ❓
- 낮은 성능의 서버로 인해 서버에서 인증 정보를 보유하고 처리하는 Session의 방식보다는 Client에서 인증 정보를 보유하고 서버에서는 인증 여부만 판별하는 식으로 서버의 부하를 줄여보려고 한다.
- 추후, 소셜 로그인(OAuth)를 도입할 예정이라 JWT 방식을 채택하고 있는 구글, 카카오, 네이버 등의 확장성을 고려하여 개발하려고 한다.
- 이번 프로젝트는 다중 로그인, 계정 공유에 대해서 고려하지 않아도 되는 플랫폼이기에 세션 방식으로 사용자들의 디바이스를 컨트롤할 필요가 없다.
JWT(JSON Web Token)
- Token 기반 인증 시스템은 Client가 로그인을 하게 되면 서버에서 인증되었다는 Token을 Client에게 발급한다.
- Token은 유일한 값이며 Client가 발급받은 Token을 요청 Header에 담아 서버에 요청을 보낼 때마다 보내게 된다.
- Client가 URL을 통해 홈페이지에 접속한다.
- Server는 Index.html을 Client에게 전송한다.
- Client가 ID, PW를 이용하여 로그인 요청을 Server에게 보낸다.
- Server는 DB에서 ID, PW가 일치하는 지 확인하고 일치한다면 Access Token과 Refresh Token을 생성하여 Client에게 전송하고 일치하지 않으면 인증되지 않았다는 에러 코드인 401을 내려보내게 된다.
- 이후, 인증된 Clinet는 API 요청을 보낼 때 Header에 Access Token을 넣어서 보내게 된다.
- Server는 Header에서 JWT 토큰을 추출하여 유효한 Access Token인지를 판단하고 요청에 맞는 응답을 내려주게 된다.
- 만약 Access Token이 만료되었다면, Client는 유효한 Refresh Token을 이용하여 Access Token을 재발급하기 위해 서버로 전송하고 Server는 이를 받아 검증하고 Access Token을 새로 발급하여 내려주게 된다.
JWT(JSON Web Token) 구조
위의 섹션에서 JWT를 이용하여 Server와 사용자간의 인증 과정을 살펴보았다. 그러면 JWT는 어떠한 구조이며 무슨 정보를 가지고 있기에 Server가 검증을 할 수 있을까에 대한 의문이 생길 것이다.
헤더(Header) 페이로드(Payload) 서명(Signature) {
"alg": "HS256",
"typ": "JWT"
}{
"sub": "1234567890",
"name": "cks._.hong",
"iat": 456465411,
}HMACSHA256(
base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret)- Header에는 JWT의 타입과 해시 알고리즘의 종류가 담겨 있다.
- Payload에는 서버에서 작성한 사용자 권한 정보와 데이터가 담겨 있다.
- Signature에는 Header, Payload를 Base64 URL-safe Encode를 하고 Header에 명시한 해시함수를 이용하여 해시 값과 서버의 개인키(Private Key)로 서명한 전자 서명이 담겨져있다.
Header
{ "alg": "HS256", "typ": "JWT" }
- alg - 서명 암호화 알고리즘(HMAC SHA256, RSA SHA256 등)
- typ - 토큰 유형
Payload
{ "sub": "1234567890", "name": "cks._.hong", "iat": 6545641654 }
- key-value 형식으로 한 쌍을 이루고 있으며 Claim이라고 지칭한다.
- Server와 Client가 통신을 할 때, 필요한 정보들을 담고 있다.
- Payload는 정해진 데이터 타입은 없지만, Registered Claims, Public Claims, Private Claims로 나누곤 한다.
{ "jti": "9784", // Registered Claim "exp": "4651654654654", // Registered Claim "https://onedayonepost.tistory.com": true, // Public Claim "username": "cks._.hong" // Private Claim }
- Registered Claims - 미리 정의된 클레임
- iss(issuer) - 토큰 발행자
- exp(expireation time) - 토큰 만료 시간
- sub(subject) - 제목
- iat(issued At) - 토큰 발행 시간
- jti(JWI ID) - 토큰 고유 식별자
- Public Claims - 사용자 지정 클레임으로서 공개용 정보 전달을 위해 사용
- Private Claims - Server와 Client 사이에 정보 공유를 위해 사용되는 것으로 사용자 지정 클레임이며 Client를 특정할 수 있는 값인 ID가 주로 담기며 외부에 공개되어도 괜찮은 정보가 담긴다.
Signature
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), your-256-bit-secret )
- Signature에서 사용하는 알고리즘은 Header에서 정의한 알고리즘 방식(alg)을 사용한다.
- Signature의 구조는 인코딩된 header + payload와 서버가 보유한 유일한 Secret Key를 합려 정의한 알고리즘으로 암호화 하게 된다.
Signature = HS256(base64UrlEncode(header) + . + base64UrlEncode(payload), Secret Key)
Header, Payload의 경우 단순 인코딩이기 때문에 보안적인 측면에서 복호화, 조작이 가능한데 Signature는 Secret Key를 서버에서 관리하기 때문에 유출되지 않는 이상 보안성이 우수하여 토큰 위변조 여부를 확인할 수 있다.
JWT(JSON Web Token)이 위·변조에 대응할 수 있는 이유
유저의 JWT가 A(Header), B(Payload), C(Signature)로 이뤄져 있다고 가정해보려고 한다.
- 임의의 해커가 JWT 토큰의 일부를 수정하여 새로운 JWT 토큰인 A(Header), B'(Payload), C(Signature)를 만들어 Server에 요청을 하였다.
- Server는 JWT 토큰을 유효성 검사를 하게 되는데 JWT에 있는 C(Signature)를 이용하여 검사를 하게 된다.
- Signature는 기존 토큰의 A, B, Secret Key의 값을 합쳐 해시 알고리즘을 사용하여 출력된 값이기 때문에 중간에 위 ·변조가 될 시 값이 변경되어 조작되었음을 확인할 수 있다.
Access Token / Refresh Token
Access Token
클라이언트가 보유하고 있는 JWT 토큰으로 유저의 정보가 담겨져 있으며 API 요청을 보낼 때 해당 토큰을 Authorization header에 'Bearer (JWT토큰)' 형식으로 Server에게 전송한다.
Refresh Token
만료된 Access Token을 새로 발급해주기 위한 토큰으로 Client의 첫 인증때 Access Token과 Refresh Token을 생성하게 된다. Client는 LocalStoreage에 보통 저장하곤 하며 Server는 데이터베이스에 유저의 정보와 맵핑시켜 저장한다. 만약, 클라이언트로부터 만료된 Access Token을 받으면 검증 후에 새로운 Access Token을 Client에게 전송해준다.
Access Token과 Refresh Token으로 나눈 이유
- JWT 토큰은 Client에서 관리하기 때문에 제 3자에 의해 토큰이 탈취될 가능성이 존재한다.
- 탈취된 JWT 토큰은 만료되기 전까지 유효한 토큰이되어 발급 받은 유저의 권한을 획득할 수 있게 된다.
- 이러한 문제점을 개선하기 위해 짧은 유효 기간을 가진 Access Token이 등장하게 되었지만 짧은 유효기간을 가지면 유저가 만료된 토큰을 발급받기 위해 계속해서 로그인을 시도해야 한다.
- 그래서 유효 시간이 짧으면서 로그인을 계속해서 시도하지 않는 방법이 무엇이 있을까라는 생각을 하다가 나온 것이 긴 유효 시간을 가진 Refresh Token이다.
- Client는 Access Token이 만료되었을 때, 유효한 Refresh Token과 만료된 Access Token을 Server에 보내게 되고 Server는 DB에 있는 해당 사용자의 Refresh Token 값과 비교하여 검증하고 새로운 AccssToken을 발급하여 Client에게 전송하게 된다.
- 정리를 해보면, 해커의 토큰 탈취를 우려하여 짧은 유효 기간의 Access Token을 구상하였고 이로 인해 발생하는 잦은 Client의 로그인은 Refresh Token의 생성으로 해결했다고 볼 수있다.
- 이러한 보안을 구상했다고 해도 Refresh Token도 탈취되면 무용지물이 아닌가라는 의문이 들어 검색을 해보았는데 다른 블로그에서 해답을 얻을 수 있었다.
JWT(JSON Web Token) 인증 전체 과정
- Client가 ID, PW를 입력하고 로그인 요청을 Server에게 보낸다.
- Server는 사용자 인증을 진행하고 Access Token과 Refresh Token을 발급하여 Client에게 전송한다.
- Client는 이를 받아 Cookie나 LocalStorage에 저장하게 된다.
- Client는 API 요청을 보낼 때, Authorization header에 'Bearer (Access Token)' 형식으로 삽입하여 Server에게 전송한다.
- Server는 이를 검증하고 유효한 Access Token이면 요청에 맞는 응답을 전송하고 만료되었다면 Access Token 만료 신호를 Client에게 보낸다. 토큰의 유효성을 검증할 때, 아래 4가지 경우의 수가 존재한다.
- Access Token과 Refresh Token 모두가 만료된 경우 => 재 로그인
- Access Token이 만료되고 Refresh Token은 유효한 경우 => Refresh Token을 검증하여 Access Token을 재발급
- Access Token이 유효하고 Refresh Token이 만료된 경우 => Access Token을 검증하여 Refresh Token을 재발급
- Access Token과 Refresh Token 모두가 유효한 경우 => 정상 응답
- Client는 Access Token의 만료를 확인하고 재발급을 위해 Access Token과 Refresh Token을 Server에게 전송하여 발급 요청을 보낸다.
- Server는 Access Token으로부터 사용자의 정보를 꺼내고 이를 바탕으로 DB를 확인하여 Refresh Token의 유효성을 검증한다.
- 유효한 Refresh Token이라면 새로운 Access Token을 발급하고 만료되었다면 다시 로그인을 하도록 Client에게 응답하게 된다.
Client에서도 Access Token의 Payload를 통해서 만료를 확인할 수 있어서 일부 과정을 생략하도록 할 수 있다.
전체적인 JWT 인증 흐름에 대해서 이해할 수 있었지만 나는 2가지 의문점이 생기게 되었다.
- 1. Access Token이 만료되었을 때만, Refresh Token을 보내게 되는데 AccessToken이 유효하고 Refresh Token이 만료된 경우가 서버에서 발생할 수 있는 가?
- 2. Access Token이 새롭게 발급될 때, Refresh Token도 새롭게 발급하게 되는 가?
이러한 의문은 프로젝트에서 로그인을 구현할 때, 다른 사람들의 코드와 블로그를 찾아보며 다시 공부해볼 생각이다.
'프로젝트 > Share Your Trip' 카테고리의 다른 글
[Spring boot] Spring Security + JWT + Redis (1/3) (0) 2024.02.05 [Spring boot] @Builder (0) 2024.01.14 [Spring boot] Password 암호화 (0) 2024.01.11 [Java] Wrapper 클래스 (0) 2024.01.07 [Java] Optional 클래스 (1) 2024.01.06