이번 글에서는 jjwt(Java Json Web Token)라는 오픈소스 라이브러리를 사용해서 JWT를 생성하고 검증하는 방법을 알아보겠습니다.
jjwt 사용 방법을 이해하실 수 있도록 기본적인 JWT 생성 예시와 JWT 검증 예시, 테스트 코드를 작성해 보았습니다.
https://jwt.io/libraries 👈여기에서 JWT를 생성하고 검증하기 위해 제공되는 오픈소스 라이브러리들을 찾을 수 있습니다.
다양한 라이브러리가 있지만 우리가 이번에 사용해 볼 라이브러리는 jjwt입니다.
만약 JWT에 대해서 잘 모르신다면 JWT의 구조를 이해하신 뒤에 이번 글을 읽으시기를 추천 드립니다.
👉 JWT 구조 이해하기
jjwt 사용 방법
jjwt 라이브러리 추가
이번 글에서는 gradle 기준으로 작성하겠습니다. 프로젝트에서 gradle을 사용하는 경우 아래와 같이 라이브러리를 추가해 줄 수 있습니다. Installation에 대한 자세한 정보는 jjwt Repository의 README.md를 참고해주세요.
dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5',
// Uncomment the next line if you want to use RSASSA-PSS (PS256, PS384, PS512) algorithms:
//'org.bouncycastle:bcprov-jdk15on:1.70',
'io.jsonwebtoken:jjwt-jackson:0.11.5' // or 'io.jsonwebtoken:jjwt-gson:0.11.5' for gson
}
jjwt 의존 관계 이해하기
jjwt-api는 토큰 생성을 위한 객체들을 추상화 하기 위한 라이브러리입니다. jjwt 라이브러리를 사용하는 클라이언트의 코드를 인터페이스에 의존하게 함으로써 의존성을 역전 시키고 내부 로직을 캡슐화 하고 있습니다. 덕분에 jjwt를 사용하는 클라이언트들은 구현에 의존하지 않는 코드를 작성할 수 있습니다. 그래서 jjwt의 내부 로직이 변경되더라도 클라이언트의 코드는 수정하지 않아도 됩니다.
JWS 생성
jjwt 라이브러리에 포함된 Jwts 구현체를 통해서 JWT를 쉽게 생성할 수 있습니다. Jwts 클래스는 JWT 인스턴스를 생성하는 역할을 하는 팩토리 클래스 입니다. Jwts 클래스를 통해서 필요한 클레임 셋을 만들고 secretKey와 함께 서명해서 JWS를 생성할 수 있습니다. 참고로 JWS와 JWE는 JWT의 종류에 속하며 클레임 셋이 암호화 된 JWT는 JWE(Java Web Encryption), 클레임 셋이 암호화 되지 않은 JWT는 JWS(Java Web Segniture)가 됩니다. 대부분 JWT라고 말하면 JWS를 칭하는 편입니다. 이 글에서도 JWT를 JWS라고 생각하고 읽어 주세요.
JWT 생성 예시
어떤 사용자를 인증하기 위한 토큰인지, 어떤 권한을 가지고 있는지, 토큰 만료 일시가 지났는지 확인하기 위한 key-value를 페이로드(payload)에 담아서 토큰을 생성하는 코드입니다.
public JwtToken createJwt(String userPk, MemberRole role) {
Claims claims = Jwts.claims().setSubject(userPk); // sub
claims.setIssuedAt(date.instance()) // iat
claims.setExpiration(new Date(date.nowTime() + REFRESH_TOKEN_VALID_MILLISECOND)) // exp
claims.put("role", role);
String jwt = Jwts.builder()
.setClaims(claims)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
return jwt;
}
sub, iat, exp 클레임은 RFC7519에 미리 정의되어 있는 Registered Claim 입니다. sub 클레임(Subject)은 JWT의 주체인 Principal 식별자 값을 나타내고. iat 클레임(IssuedAt)은 토큰 발행 일시, exp 클레임(Expiration)은 토큰 만료 일시를 나타냅니다. role 클레임은 제가 임의로 추가한 클레임입니다.
이러한 Registered Claim들은 라이브러리에서 쉽게 값을 세팅할 수 있도록 setIssuedAt, setExpiration과 같은 Setter들을 제공하고 있는 것을 볼 수 있습니다.
JWT 검증
아래의 내용을 이해하려면 JWT의 구조에 대한 이해가 필요합니다.
다음 글에서 JWT의 구조에 대해 설명하고 있습니다. jwt 구조 이해하기 👈click!!
유효한 JWT인지 검증하는 것은 JwtParser 구현체가 담당하는 역할입니다. parseClaimsJws 메서드를 호출하면 기본적인 포맷을 검증하고, jwt를 생성할 때 사용했던 secretKey로 서명했을 때 토큰에 포함된 signature와 동일한 signature가 생성되는지 확인합니다. Header.Payload에 대해서 동일한 secretKey로 서명했을 때 생성된 Signature는 항상 같아야 합니다. 만약 다르다면 Header.Payload의 값이 변조되었다고 판단할 수 있겠죠. 즉, JWT가 유효한지 판단하기 위해서는 secretKey가 필요합니다.
JWT 검증 예시
public boolean isValidToken(JwtParser jwtParser) {
try {
Jws<Claims> claims = jwtParser.parseClaimsJws(value);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
jwtParser의 parseClaimsJws 메서드는 서명된 JWS(Jason Web Signature)를 검증하고 클레임 셋을 반환합니다.
그리고 토큰의 유효 기간을 확인하기 위해서 클레임 셋에 포함된 exp 클레임을 가져와 토큰 만료 일시와 현재 일시를 비교합니다.
parseClaimsJws 메서드가 던질 수 있는 예외는 다음과 같습니다.
ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException
예제 코드는 Exception으로 모든 예외를 잡았지만, 각 예외에 대해서 적절한 처리를 해주는 것이 바람직합니다.
JWT 생성 & 검증 테스트 예시
jjwt로 토큰을 생성하고 검증을 거친 클레임 셋을 반환받아 클레임을 잘 파싱하고 있는지 확인할 수 있는 테스트 코드입니다. 어떻게 동작하는지 이해하는 데 도움이 되시기를 바랍니다.
@Test
@DisplayName("jjwt를 사용해서 유효한 JWT를 생성하고 검증합니다")
void jjwtTest() {
Claims claims = Jwts.claims();
claims.setSubject("greenneuron");
Date iat = new Date();
claims.setIssuedAt(iat);
Date exp = new Date(iat.getTime()+1000*60*60*24);
claims.setExpiration(exp);
String random256BitKey = "6v9y$B&E)H@MbQeThWmZq4t7w!z%C*F-";
SecretKey secretKey = Keys.hmacShaKeyFor(random256BitKey.getBytes());
JwtParser parser = Jwts.parserBuilder().setSigningKey(secretKey).build();
String jws = Jwts.builder()
.setClaims(claims)
.signWith(secretKey, SignatureAlgorithm.HS256)
.compact();
Jws<Claims> claimsJws = parser.parseClaimsJws(jws);
assertThat(claimsJws.getBody().getSubject()).isEqualTo("greenneuron");
assertThat(claimsJws.getBody().getExpiration().before(iat)).isFalse();
}
참고 자료
- jjwt Repository의 README.md
: https://github.com/jwtk/jjwt