본문 바로가기
웹개발/JWT

Java에서 JJWT(Java JSON Web Token)를 이용한 JWT(JSON Web Token) 사용방법

by 어컴띵 2021. 3. 24.

Java에서 JJWT를 이용한 JSON Web Token 사용방법을 알아본다.

 

아래의 내용은 다음 링크를 참조하여 사용방법을 필요한 부분만 참조하여 작성을 하였다.

github.com/jwtk/jjwt

 

jwtk/jjwt

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

1. JWS 생성

 

(1) JwtBuilder객체를 생성하고 Jwts.builder() 메서드를 이용한다.

(2) header 파라메터와 claims를 추가하기위해 JwtBuilder 메서드를 호출한다.

(3) JWT를 서명하기위해 SecretKey나 PrivateKey를 지정한다.

(4) 마지막으로 압축하고 서명하기위해 compact()를 호출하고, jws를 생성한다.

 

다음과 같이 생성한다.

String jws = Jwts.builder()          // (1)
                  .setSubject("Joe") // (2)
                  .signWith(key)     // (3)
                  .compact()         // (4)

 

Header parameters

JWT Header는 JWT의 claims와 관련된 컨텐츠, 형식, 암호화 작업에 대한 메타데이터를 제공한다,

JWT 파라메터는 하나이상 JwtBuilder setHeaderPara메소드로 한번 혹은 여러번 설정할 수 있다.

String jws = Jwts.builder()
             .setHeaderParam("kid", "myKeyId")
             .....

setHeaderParam을 호출하면 key-value 쌍으로 header에 추가하고, 기존에 key가 존재하면 값을 덮어 쓴다. 

 

참고: alg, zip헤더는 자동으로 설정하므로 설정할 필요가 없다.

 

Header Instance

한번에 header를 지정하기 원하면 Jwts.header() 메서드를 사용한다.

Header header = Jwts.header();

populate(header);

String jws = Jwts.builder()
             .setHeader(header)
             ............

참고: setHeader를 호출하면 headr에 이미 있는 key의 value를 덮어쓴다. alg 및 zip도 마찬가지이다.

 

Header Map

header를 한번에 셋팅하기를 원하고 Jwts.header()를 사용하고 싶지 않느면, JwtBuilder setHeader(Map) 메소드를 대신 사용 할수 있다.

Map<String, Object> header = getMyHeaderMap();
String jws = Jwts.builder()
             .setHeader(header)
             ...................

참고: setHeader를 호출하면 header에 이미 있는 key의 value를 덮어쓴다. alg 및 zip도 마찬가지이다.

 

Claims

Claims는 JWT의 body이고 JWT 생성자가 JWT를 받는이들에게 제시하기 바라는 정보를 포함한다.

Standard Claims

JwtBuilder는 JWT스펙에 정의한 기본으로 등록된 Cliam names에 대해서 다음과 같은 편리한 setter 메서드를 제공한다.

  • setIssuer: iss (Issuer) Claim
  • setSubject: sub (Subject) Claim
  • setAudience: aud (Audience) Claim
  • setExpiration: exp (Expiration Time) Claim
  • setNotBefore: nbf (Not Before) Claim
  • setIssuedAt: iat (Issued At) Claim
  • setId: jit(JWT ID) Claim

 

다음과 같이 설정한다.

String jws = Jwts.builder()
             .setIssuer("me")
             .setSubject("Bob")
             .setAudience("you")
             .setExpiration(expriation) // a java.util.Date
             .setNotBefore(notBefore) // a java.util.Date
             .setIssuedAt(new Date()) // for example, now
             .setId(UUID.randomUUID()) // just an example id
             ................

Custom Claims

기본으로 등록된 클레임에 없는 사용자가 지정하는 클레임을 설정하고 싶을때 JwtBuilder claim 메소드를 이용한다.

String jws = Jwts.builder()
             .claim("hello", "world")
             ...............

claim이 호출되면 Claims인스턴으세 key-value쌍의 데이타가 추가되며 기존에 있는 key-value는 덮어쓴다.

명백하게 기본으로 등록된 claim이름에 대해서 claim을 호출할 필요가 없고, 가독성을 위해서 setter 메소드를 사용하는걸 추천한다.

 

Claims Instance

모든 Claim을 한번에 지정하고 싶다면 Jwts.claims() 메소드를 사용하여 claims인스턴스를 받아서 작업하면 된다.

Claims claims = Jwts.claims();
populate(claims); //implement me
String jws = Jwts.builder()
             .setClaims(claims)
             ....................

참고: setClaims를 호출하면 기존에 있는 같은 이름의 claims를 덮어쓴다.

 

Claims Map

Jwts.claims()를 사용하지 않고 한번에 clims를 지정하려면 JwtBuilder setClaims(Map) 메소드를 사용할수 있다.

Map<String, Object> claims = getMyClaimsMap(); // implment me
String jws = Jwts.builder()
             .setClaims(claims)
             ....................

참고: setClaims를 호출하면 기존에 있는 같은이름의 claims를 덮어쓴다.

 

Signing Key

JwtBuilder의 signWith 메소드를 호출하여 sign key를 지정하고, JJWT가 지정된 key에 허용된 가장 안전한 알고리즘을 결정하도록 하는게 좋다.

String jwt = Jwts.builder()
             .........
             .signWith(key) // <--
             .compact();

예를 들어 256bit(32bytes)길이의 secretKey를 사용하여 signWith를 호출하면 HS384나 HS512에 비해 충분치 않으므로, JJWT는 HS256을 이용하여 JWT를 자동으로 서명할것이다.

 

signWith를 사용할때 JJWT는 연관된 알고리즘 식별자와 함께 alg header도 자동으로 설정한다.

 

비슷하게 4096bit 길이의 RSA PrivateKey를 가지고 signWith를 호출하면, JJWT는 RS512알고리즘을 사용할려고 하고 alg header에는 RS512를 자동으로 설정한다.

 

Elliptic Curve PrivateKey(타원곡선암호와?)에도 같은 방식이 적용된다.

 

참고: publicKey를 이용한 JWT 서명은 항상 불안정하기 때문에 사용할수 없다. JJWT는 어떤 publicKey이용한 서명도 InvalidKeyException을 던지고 거부한다.

 

Secret Formats

HMAC-SHA 알고리즘과 문자열 secret key 또는 인코딩된 byte array를 사용해서 JWT에 서명하는 경우 signWith 메소드 인수로 사용할 SecretKey 인스턴스로 변환해야 한다.

 

  • 인코딩된 byte array:
SecretKey key = Keys.hmacShaKeyFor(encodedKeyBytes);
  • Base64 인코됭된 문자열
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretString));
  • Base64URL 인코딩된 문자열
SecretKey key = keys.hmacShakeyFor(Decoders.BASE64URL.decode(scretString));
  • 인코딩 안된 문자열(예 password 문자열)
SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));

secretString.getBytes()시에는 항상 캐릭터셋을 지정해야한다.

그리고 식별할수 있는 문자열 pasword는 가능한 피해야한다. 가능하면 안전한 랜덤키를 생성해서 사용하는것이 좋다.

참조: github.com/jwtk/jjwt#jws-key-create-secret

 

jwtk/jjwt

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

SignatureAlgorithm Override

종종 주어진 key에 대해서 기본 알고리즘을 override해서 사용하고 싶을수 있다.

예를 들면 2048bit의 RSA privateKey가 있다면, JJWT는 자동으로 RS256알고리즘을 선택한다. 그대신 RS384 또는 RS512를 사용하고 싶으면 추가 파라메타로 SignatureAlgorithm를 허용하는 sighWith메소드의 인자로 넘긴다.

.sighWith(privateKey, SignatureAlgorithm.RS512) // <--
.compact();

JWT 스펙은 2048bit보다 큰 어떤 RSA key에 대해서든 RSA 알고리즘을 허용한다. JJWT는 단지 4096bit보다 큰 key는 RS512를, 3072bit 보다 큰 key는 RS384를 그리고 2048보다 큰 key는 RS256은 선호한다.

 

모든 케이스에 대해서 어쨋든 , 선택된 알고리즘과 상관없이, JJWT는 지정된 키가 JWT 스펙요구사항에 따른 알고리즘이 사용될수 있다고 주장한다.

 

JWS Compression

JWT claims 에 많은 데이타가 있다면, 그리고 JJWT가 같은 라이브러리에서 JWS를 읽거나 파싱하다면 JWS를 압축하여 크기를 줄일수 있다. 참고로 이건 JWS의 표준 기능이 아니고, 다른 JWT 라이브러리에서 지원되지 않을수 있다.

 

JWT를 압축 및 압축해제하는 방법은 Compression 장을 보길 바란다.

 

2. Reading a JWS

다음과 같이 JWS를 읽어온다.

1. Jwts.parseBuilder메서드를 이용해서 JwtParseBuilder인스턴스를 생성한다.

2. JWS 서명 검증을 위한 SecretKey 혹은 비대칭 PublicKey를 지정한다.

3. 스레드에 안전한 JwtPaser를 리턴하기 위해 JwtPaserBuilder의 build()메서드를 호출한다.

4. 마지막으로 원본 JWS를 생성하는 jws를 가지고 parseClaimsJws(String)메서드를 호출한다.

5. 파싱이나 서명검증오류 경우에 try/catch구문으로 전체를 감싼다. 예외와 실패 원인은 나중에 다룬다.

Jws<Claims> jws;

try{
    jws = Jwts.parseBuilder() // (1)
    .setSigningKey(key)       // (2)
    .build()                  // (3)
    .parseClaimsJws(jwsString)// (4)
    ...............
} catch (JwtException ex) {   // (5)

}

참고: JWS를 기대한다면 항상 JwtParser의 parseClaimsJws 메서드를 호출한다.(그리고 사용 가능한 다른 유사한 메소드중 하나가 아님) 서명된 JWT 파싱에 대한 정확한 보안 모델을 보장한다.

 

Verification Key

JWS를 읽을때 하는 가장 중요한 것은 JWS의 암호화된 서명을 검증하기 위해 사용하는 지정된 키이다. 서명 검증에 실패하면 JWT은 신뢰할수 없고, 폐기되어야 한다.

 

그래서 검증을 위해서 어느키를 사용해야하나?

  • JWS가 SecretKey키로 서명되었으면, 같은 SecretKey가 JwtParserBuilder에 지정되어야 한다.
Jwts.parserBuilder()
    .setSigningKey(secretKey) // <--
    .build()
    .parseClaimsJws(jwsString);
  • JWS가 PrivateKey로 서명되었으면 그key에 대응하는 PublicKey(PrivateKey가 아닌)가 JwtParserBuilder에 지정되어야 한다.
Jwts.parserBuilder()
    .setSigningKey(publicKey) // <-- publickey, not privatekey
    .build()
    .parseClaimsJws(jwsString);

하지만 어떤 의문이 들수 있다. 만일 어플리케이션이 single SecretKey혹은 KeyPair을 사용하지 않는다면? JWS가 다른 SecretKey나 public/private key들로 생성이 되어다면, 혹은 양쪽다라면? JWT를 먼저 검사할수 없다면 어떻게 어떤 키를 지정할지 알수 있는가? 

 

이런경우 JwtParserBuilder의 setSigningKey메서드를 호출할수 없다. 대신에 SigningKeyResolver를 사용할 필요가 있다. 바로 다음에 다룬다.

 

Signing Key Resolver

어플리케이션이 다른 key로 서명될수 있다면, setSigningKey메서드를 호출할수 없다. SigningKeyResolver 인터페이스를 구현하고 JwtParserBuilder의 setSigningKeyResolver 메서드에서 인스턴스를 지정해야 한다.

SigningKeyResolver signingKeyResolver = getMySigningKeyResolver();

Jwts.parserBuilder()
    .setSigningKeyResolver(signingKeyResolver) // <--
    .build()
    .parseClaimsJws(jwsString);

SigningKeyResolverAdapter을 상속받고 resolveSigningKey(JwsHeader, Claims) 메서드를 구현하는 작업으로 간단하게 할수 있다. 다음 코드 처럼

public class MySigningKeyResolver extends SigningKeyResolverAdapter {

    @Override
    public Key resolveSigningKey(JwsHeader header, Claims claims) {
        // implement me
    }
}

JwtParser는 JWS JSON을 파싱하고 jws 서명을 확인하기 전에 resolveSiginingKey를 호출한다. 이를 통해 특정 jws를 확인하는데 사용할 key를 찾는데 도움이 되는 정보에 대해 JwsHeader 및 Claims 인수를 검사할수 있다. 이것은 서로다른 시간또는 다른유저, 또는 고객이 다른 키를 사용하는 또는 복잡한 보안 모델의 어플리케이션에 매우 강력하다 

 

어떤 데이타를 검사할수 있나?

JWT 스펙은 JWS가 생성될때 JWS header에 kid(Key ID)필드를 설정할수 있는 방법을 제공한다. 다음과 같이

Key singingKey = getSigningKey();
String keyId = getKeyId(signingKey); // 
String jws = Jws.builder()
             .setHeaderParam(JwsHeader.KEY_ID, keyId) // 1
             .signWith(signingKey) // 2
             .compact();

파싱하는 동안 SigningKeyResolver은 데이타베이스 처럼 어떤 장소로 부터 key를 찾아서 kid 를 얻어서 JwsHeader를 검사할수 있다.

public class MySigningResolver extends SigningKeyResolverAdapter {
    
    @Overried
    public Key resolveSigningKey(JwsHeader header, Claims claims) {
        // header또는 claims를 검사하고, 찾고 signing key를 반환한다.
        String keyId = header.getKeyId(); // 필요한 경우 다른 필드도 검사한다.
        Key key = lookupVerificationKey(keyId); // 구현한다.
        return key;
    }
}

참고로 jwsHeader.getKeyId()는 가장 일반적으로 key를 찾는 방법일뿐다. 검증된 key를 찾기위해 여려 header 필드 또는 claims에서 확인할수 잇다. JWS가 어떻게 만들어 졌는지가 기반이 된다.

 

마지막으로 HMAC algorithm인 경우 반환된 key는 SecretKey여야 하고, 비대칭 알고리즘의 경우, 반환된 key는 PublicKey(PrivateKey가 아닌)여야 한다.

 

Claim Assertions

JWS를 파싱할때 어플리케이션에서 준수해야하는 조건을 강제할수 있다.

 

예를 들어 지정된 sub(Subject) 값을 파싱할때 필요한경우 JwtParserBuilder의 require* 메서드를 사용하면 된다.

try{
    Jwts.parserBuilder().requireSubject("jsmith").setSigningKey(key).build().parseClaimsJws(s);
} catch(MissingClaimException ex) {
    // sub 필드가 누락되었거나 jsmith라는 값이 없다.
}

필드 누락과 잘못된 값에 대한 대응이 중요하면 InvalidClaimException 예외를 잡는다. MissingClaimException 또는 IncorrectClaimException 예외를 잡느다.

try {
    Jwts.parseBuilder().requireSubject("jsmith").setSigningKey(key).build.parseClaimsJws(s);
} catch (MissingClaimException ex) {
    // sub 필드가 없을때
} catch (IncorrectClaimException ex) {
    // sub 필드가 있지만 값이 'jsmith'가 아닐때
}

사용자정의 claim필드에 대해서도 require(fieldName, requiredFiledValue) 메서드를 이용해서 요구할수 있다.

try {
    Jwts.parseBuilder()
        .required("myfield", "myRequiredValue")
        .setSigningKey(key)
        .build()
        .parseClaimJws(s);
} catch (InvalidClaimException ex) {
    // myfield가 없거나 myfield값이 'myRequiredValue' 가 아닌경우
}

(그리고 MissingClaimException 또는 IncorrectClaimException으로 예외를 잡을수 있다.)

 

JavaDoc에서 JwtParserBuilder클래스의 required* 메서드의 전체 리스트를 볼수 있다.

 

JJWT를 이용한 JSON Web Token의 구현을 알아 보았다. 여기서는 필요한 것 같은 내용만 살펴 보았으니 더많은 내용을 보려면 다음을 참조하기 바란다.

 

github.com/jwtk/jjwt

 

jwtk/jjwt

Java JWT: JSON Web Token for Java and Android. Contribute to jwtk/jjwt development by creating an account on GitHub.

github.com

 

'웹개발 > JWT' 카테고리의 다른 글

Java에서 JSON Web Token 생성하고 검증하기  (0) 2021.03.25
JWT(JSON Web Token)  (0) 2021.03.22