java jjwt 이해하기
JWT를 사용하기로 결정하였을 때, jjwt 라이브러리를 선택하여 학습하고 공식문서의 가이드라인에 맞게 리팩토링하는 과정을 진행했습니다. github readme를 기반으로 학습하였는데 공식문서의 설명이 매우 친절하였으며 제가 사용할 JWT 토큰에 대한 이해도를 높일 수 있었습니다.
1. jjwt 선택
여러 JWT 라이브러리가 존재하는데 jjwt를 선택한 이유는 먼저, 2024년까지도 버전업
을 하고 있으며 공식문서가 친절
하고 커뮤니티 활성도가 높다
는 점에서 선택하게 되었습니다. 또한, 학습 비용이 다른 라이브러리보다 적다
는 점에서 선택하였습니다. 다른 라이브러리들은 0 부터 시작해서 학습해야하지만 jjwt는 Spring Boot를 학습하며 접해왔기 때문에 다른 라이브러리보다 조금이라도 경험치가 있다고 판단하였습니다.
2. JJWT가 제공하는 JWT 종류
jjwt에서 제공하는 jwt 종류는 JWE, JWS, encrypted JWTs, JWK가 있습니다.
그중 encrypted JWTs와 JWk 그리고 JWE는 개념적인 선에서만 살펴보고 JWS에 대해서 집중적으로 살펴보겠습니다.
JWE는 서명이 없는 JWT입니다. 즉, 데이터를 전달하는 용도로 사용하는 JWT라고 보면됩니다. encrypted JWTs
는 JWT의 payload를 암호화하여 전달하는 JWT입니다. 짧게 살펴본 바로는 외부 서버와 통신할 때 중요한 데이터를 감추고 싶을 겨우 사용하는 방식이라고 판단하였습니다. JWK
는 토큰을 비대칭키로 서명하면 공개키를 사용자에게 전달해야 하는데 이 공개키를 클라이언트에게 제공하는 JWT입니다. 여러 비대칭키를 관리하는 경우 JWKS를 이용하여 용이하게 관리할 수 있습니다.
JWE,JWK,encrypted JWTs에 대해서 짧게 학습한 이유는 실제 서비스에서 사용할 곳을 아직 생각해내지 못했고 따라서 지금 자세하게 학습하더라도 죽은 지식이 될 것이라 생각하여 JWS에 집중하였습니다.
JWS
는 우리가 흔히 말하는 JWT의 개념에 가깝습니다. Header,Payload가 존재하고 서명이 들어가는 것이 바로 JWS입니다.
3. jjwt 서명 알고리즘
JWS에는 서명을 위해 13개의 알고리즘이 제공됩니다. 그 중 3개는 Secret Key 방식이고 10개는 비대칭 키 방식입니다. 비대칭 키 방식은 Secret Key보다 암/복호화하는 비용이 더 들어가기 때문에 성능적인 면에서 해싱함수를 사용하는 Secret Key 방식을 선택하였습니다.
Secret Key 방식에는 HS256, HS384, HS512가 있는데 이들은 HMAC - SHA 알고리즘으로 총 해싱을 2번 진행하는 알고리즘입니다.
HS256을 예시로 해싱 과정을 간단하게 설명하면
비밀키를 특정 상수 A 값과 XOR 연산을 진행한 값을 바탕으로 header + payload와 결합하여 SHA-256 해시함수에 입력합니다.
비밀키를 특정 상수 B 값과 XOR 연산을 진행한 값을 바탕으로 1번 해시 함수 결과와 결합하여 다시 SHA-256 해시 함수에 입력합니다.
따라서 총 2번의 해싱과정이 일어나는 것이 바로 HMAC - SHA 입니다. 이 과정은 간단하게 설명한 것이라 내부의 동작 과정은 더 자세하겠지만, 어떤 과정을 거쳐 결과값이 나오는지에 대한 이해를 목적으로 학습하였습니다.
이렇게 2번에 걸쳐 해싱 과정이 일어나게 되어 Secret Key가 노출 될 위험이 현저히 적어지게 됩니다. 즉, 해커가 진행하는 무차별 공격에 대한 저항력이 높은 알고리즘이라고 볼 수 있습니다. HS 뒤의 숫자는 Secret Key의 필수 비트 수를 의미합니다.
HS256의 경우 256bit(32byte)이상의 SecereKey가 필요하며 HS384는 384bit(48byte), HS512는 512bit(64byte)이상의 Secret Key가 필요합니다.
JJWT를 생성할 때 제공하는 key 길이에 따라 선택되는 알고리즘이 결정됩니다. 예를 들면 64 byte의 키를 사용한다면 JWT 토큰에 서명할 때 자동적으로 HS512 알고리즘으로 서명을 진행합니다. 만약, 32Byte의 키가 제공되면 HS256 알고리즘으로 서명을 진행합니다.
여기서 그럼 어떤 서명 알고리즘을 선택해야할까요?
그 근거는 벤치마크 성능지표와 보안을 기준으로 선택해볼 수 있었습니다. 먼저 보안적으로는 길이가 더 긴 해시를 생성하는 HS512가 무차별 대입 공격에 대한 저항력이 더 높다고 볼 수 있습니다. 성능적인 면에서 살펴보면 HS256이 빠르긴 하지만 큰 차이를 내진 않는다 것을 확인했습니다. 벤치마크
따라서, 성능적인 면에서 큰차이가 없다면 보안적으로 공격에 대한 저항력이 높은 HS512를 사용하기로 결정하였습니다.
4. 토큰 생성
JWT 토큰을 생성할 때 payload에는 2개의 Claim 타입이 들어갑니다.
첫 번째는, 표준 Claims 입니다. 흔히 사용했던 expiration, issuedAt, subject등이 이에 해당합니다. 이보다 더 많은 표준 claims가 있고 이는 공식 문서에 잘 정리되어있습니다.
두 번째는, Key-Value 형식의 커스텀 Claim입니다. 이는 payload에 정해진 key값 이외의 값을 넣을 때 사용합니다.
다음은 실제 생성하는 코드입니다.
1
2
3
4
5
6
7
return Jwts.builder()
.claim("role", role)
.subject(userId)
.issuedAt(new Date(now))
.expiration(new Date(now + accessTokenExpiredTime))
.signWith(key)
.compact();
저는 기본적인 정보들은 표준 Claim을 이용하고 role의 경우 Custom claim을 사용하여 JWT를 발급하는 구조로 작성했습니다.
role을 추가한 이유는 일반적인 기능에 대해 접근할 때 role을 이용해 권한 검증을 바로하여 접근할 수 있게 하고, 중요한 보안 요구사항이 있는 로직에 대해서는 db에 접근해 role 권한이 올바르게 설정되어있는지 확인하는 방식으로 처리할 생각으로 role을 claim으로 추가하였습니다.
5. 정리하며…
그 밖에 헤더를 커스텀하는 방식, Key를 생성해주는 KeyGenerator, 여러개의 키를 관리하게 도와주는 기능 등 많은 것들이 있었는데 제가 정작 사용하는 부분은 매우 작은 부분이었습니다. 이 부분에 대한 이해도 없이 jjwt를 사용하는 것보다 확실한 이해를 바탕으로 사용하는 것이 나중에 문제가 터지거나 성능이슈가 생겼을 때 좀 더 빠르게 찾을 수 있다고 생각합니다. 또한, jjwt에서 제공하는 다른 기능들이 차후에 필요하게 되면 문서를 바탕으로 빠르게 도입할 수 있는 기반을 만들 수 있는 시간이었습니다.