경험이 많지 않아 단언할 순 없지만, REST 아키텍처로 웹 서비스를 만들다 보면 대부분 토큰 인증 방식을 스프링 시큐리티에 접목하게 된다. 클라우드 서비스를 이용하는 추세를 보면, 세션 인증 방식보다 서버 분산 등 확장성에 더 좋은 토큰 인증 방식을 선호하게 된다. 서버를 여러대 두거나, 같은 사용자가 서로 다른 도메인 데이터를 요청할 경우 세션 유지를 위한 처리(비용)이 증가하기 때문이다. 그래서 토큰을 통해 사용자의 모든 요청을 확인하는 방식을 적용하는데, 이 때 JWT 토큰을 많이 사용한다.
JWT를 학습하다보면, JWT 라이브러리에도 다양한 선택지가 있다는 것을 알 수 있고, 그럼 이제 뭘 선택해야 할지 고민에 빠지게 된다. 이 글은 그 고민의 결과물이다.
java-jwt와 jjwt
Json Web Token을 다루는 라이브러리가 몇 가지 있는데, jwt.io 사이트에서 확인할 수 있다.
java 용으로 분류한 결과이다.
이 중에서 가장 유명한 JWT 라이브러리가 auth0에서 만든 Java-jwt와 okta에서 만든 jjwt 라이브러리이다.
둘 다 Json Web Token(JWT)을 인코딩하거나 디코딩하는 기능을 제공한다.
jjwt가 상대적으로 더 많은 암호화 알고리즘을 제공하는 것을 확인할 수 있다.
기준일 2023-02-09 |
java-jwt | jjwt |
License
|
MIT license | Apache-2.0 license |
Stars | 5.1k stars | 8.9k stars |
Forks | 216 watching | 268 watching |
Watchers | 879 forks | 1.2k forks |
Github Star 갯수나 Fork 갯수로도 jjwt가 앞서고 있다.
많이 찾아보진 않았지만, java-jwt 라이브러리는 사용하기 쉽다거나 가독성이 좋아서 편하다는 평이 있다. 반면, jjwt 라이브러리는 좀 더 고급 기능과 옵션을 제공한다고 한다. (깊게 사용해 본 적이 없어서 판단하기가 쉽진 않다.)
간단한 수준에서 사용을 해봤는데, 기능적으로 거의 유사하고 구현 방식만 약간씩 차이난다.
java-jwt 라이브러리가 Base64로 encoding 된 값을 decoding 할 수 있는 JWT.decode() 메소드를 지원하고 jjwt 라이브러리는 별도로 decode 메서드를 지원하지 않는다. Base64 decode는 손쉽게 할 수 있긴 하지만, 그럼에도 불구하고 주의(?) 차원에서 의도적으로 지원하지 않는 것 같다.
auth0의 java-jwt 라이브러리
JWT 생성 - JWT.create() 메서드 사용
JWT 검증 - JWT.require() 메서드 사용
JWT 클레임 추가 - withClaim() 메서드 사용 또는 with~~~() 메서드 사용
샘플 코드
package kr.pe.petaverse.petaverseapi.jwt;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.util.Base64Utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
public class JavaJWTTest {
String secretKey = "SecretKeyToGenJWTsSecretKeyToGenJWTsSecretKeyToGenJWTs";
private void printToken(String token) {
System.out.println("token: " + token);
System.out.println("header: " + decodeToken(token.split("\\.")[0]));
System.out.println("payload: " + decodeToken(token.split("\\.")[1]));
}
private String decodeToken(String token) {
return new String(Base64Utils.decodeFromString(token));
}
@Test
void test_auth0_token() {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", "test");
claims.put("name", "dalichoi");
claims.put("admin", "true");
claims.put("exp", Instant.now().plusSeconds(60*60*24).getEpochSecond());
claims.put("iat", Instant.now().getEpochSecond());
String auth0Token = JWT.create()
// .withClaim("sub", "test")
// .withClaim("name", "dalichoi")
// .withClaim("admin", "true")
// .withClaim("exp", Instant.now().plusSeconds(60*60*24).getEpochSecond())
// .withClaim("iat", Instant.now().getEpochSecond())
// .withExpiresAt(Date.from(Instant.now().plusSeconds(60*60*24))) // exp 기본 제공
// .withIssuedAt(Date.from(Instant.now())) // iat 기본 제공
.withPayload(claims) // map 데이터를 payload로 한 번에 넣을 수도 있다.
.sign(Algorithm.HMAC256(secretKey));
printToken(auth0Token);
DecodedJWT verify = JWT.require(Algorithm.HMAC256(secretKey))
.build()
.verify(auth0Token);
System.out.println("verify: " + verify.getClaims());
}
}
okta의 jjwt 라이브러리
JWT 생성 - Jwts.builder() 메서드 사용
JWT 검증 - JWT.parserBuilder() 메서드 사용
JWT 클레임 추가 - addClaims() 메서드 사용 또는 set~~~() 메서드 사용
샘플 코드
package com.example.springsecuritystudy.jwt;
import java.time.Instant;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
import org.springframework.util.Base64Utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;
class JwtUtilsTest {
String secretKey = "SecretKeyToGenJWTsSecretKeyToGenJWTsSecretKeyToGenJWTs";
private void printToken(String token) {
System.out.println("token: " + token);
System.out.println("header: " + decodeToken(token.split("\\.")[0]));
System.out.println("payload: " + decodeToken(token.split("\\.")[1]));
}
private String decodeToken(String token) {
return new String(Base64Utils.decodeFromString(token));
}
@Test
void test_okta_token() {
Map<String, Object> claims = new HashMap<>();
claims.put("sub", "test");
claims.put("name", "dalichoi");
claims.put("admin", "true");
claims.put("exp", Instant.now().plusSeconds(60*60*24).getEpochSecond());
claims.put("iat", Instant.now().getEpochSecond());
String oktaToken = Jwts.builder()
// .setHeaderParam("typ", "JWT")
// .setHeaderParam("alg", "HS256")
// .setHeaderParam("kid", "key1")
// .setSubject("test")
// .setIssuedAt(java.util.Date.from(Instant.now()))
// .setExpiration(java.util.Date.from(Instant.now().plusSeconds(60*60*24)))
.addClaims(claims)
.signWith(Keys.hmacShaKeyFor(secretKey.getBytes()), SignatureAlgorithm.HS256)
.compact();
printToken(oktaToken);
Jws<Claims> claimsJws = Jwts.parserBuilder()
.setSigningKey(Keys.hmacShaKeyFor(secretKey.getBytes()))
.build()
.parseClaimsJws(oktaToken);
System.out.println("claimsJws: " + claimsJws);
}
}
기능적으로 워낙 비슷하다보니 개발 환경이나 취향에 따라 적절히 선택하면 될 것 같다.
'개발 > Java|Spring' 카테고리의 다른 글
Builder 패턴과 Lombok @Builder 사용 시 주의사항 (0) | 2023.02.20 |
---|---|
대량 이미지 동일한 사이즈로 분할하기(JAVA) (0) | 2023.02.13 |
Spring Security JWT 토큰으로 인증하기 (0) | 2023.02.07 |
Spring Security Custom Fiilter 적용(UsernamePasswordAuthenticationFilter를 활용해 모든 권한을 가진 tester 계정 만들기) (0) | 2023.02.07 |
인텔리제이(IntelliJ) 코드 실시간 반영(서버 자동 재시작) (0) | 2023.01.19 |