본문 바로가기
프로그래밍/기타

[JWT+JAVA] JWT(Json Web Token)에 대해서 자세히 알아봅시다 - 실습편

by 사바라다 2023. 2. 27.

 

안녕하세요. 이전 포스팅에서 JWT의 이론적인 측면에 대해서 자세히 다뤄보았습니다.

오늘은 JWT를 java에서 생성해보고 검증하는 코드를 실제로 작성해보도록 하겠습니다.

오늘 실습한 내용은 github에 올려두어 직접 테스트를 진행해보실 수 있습니다.

실습 코드 github Link

환경

아래가 오늘 jwt 실습에 사용할 라이브러리입니다. jjwt를 사용합니다.

jjwt를 사용하는 이유는 라이브러리를 선택할 때 아래의 기준으로 선택되었습니다.

  1. 표준 스펙을 대부분 지원하는가 ?
  2. project가 가장 star의 수가 많은가 ?
  3. 문서가 잘 되어있는가 ?
implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.11.5")
implementation("io.jsonwebtoken:jjwt-jackson:0.11.5")

혹시 다른 라이브러리를 사용하고 싶으신 분들은 java-jwt 라이브러리도 위의 기준에 부합 한다고 판단하였기 때문에 추천합니다.

jjwt라이브러리를 활용한 Registered Fields만 포함되어있는 signed jwt 생성 및 검증

jjwt를 이용하여 signed jwt를 만들기 위해서는 아래의 과정을 거칩니다.

  1. Jwts.builder() method를 사용하여 JwtBuilder instance를 만듭니다.
  2. JwtBuilder methods를 호출하여 header parameters와 clames를 등록합니다.
  3. JWT를 서명하는 데 사용할 SecretKey 또는 비대칭 PrivateKey를 지정합니다.
  4. 마지막으로, compact() 메서드를 호출하여 설정된 signed jwt를 생성합니다.

코드로 보면 아래와 같습니다.

SecretKey key = Keys
            .secretKeyFor(SignatureAlgorithm.HS256);

String jws = Jwts.builder()
            .setSubject(subject)
            .signWith(key)
            .compact();

테스트 코드로 보면 아래와 같습니다. registered fields의 모든 정보를 담았습니다. 해당 정보들이 어떤것인지는 [JWT] JWT(Json Web Token)에 대해서 자세히 알아봅시다 - 이론편를 참고해주면 될 것 같습니다. 아래 테스트는 jwt 생성을 할 때 exception 없이 성공하는 것을 보기위한 코드입니다. peak 부분에서 jwt가 제대로 생성된것을 확인할 수 있습니다.

@Test
public void signed_jwt_create_with_registered_fields() {
    // given
    String issuer = "Karol";
    String subject = "Auth";
    String audience = "Karol";
    Date expiredAt = Date.from(Instant.now().plus(Duration.ofDays(1L)));
    Date NotBeforeAt = Date.from(Instant.now());
    Date issuedAt = Date.from(Instant.now());
    String jwtId = UUID.randomUUID().toString();

    // when
    var key = Keys
            .secretKeyFor(SignatureAlgorithm.HS256);

    var jws = Jwts.builder()
            .setIssuer(issuer)
            .setSubject(subject)
            .setAudience(audience)
            .setExpiration(expiredAt)
            .setNotBefore(NotBeforeAt)
            .setIssuedAt(issuedAt)
            .setId(jwtId)
            .signWith(key)
            .compact();

    // peak
    System.out.println(jws); // eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJLYXJvbCIsInN1YiI6IkF1dGgiLCJhdWQiOiJLYXJvbCIsImV4cCI6MTY3NzUwNzMwOSwibmJmIjoxNjc3NDIwOTA5LCJpYXQiOjE2Nzc0MjA5MDksImp0aSI6IjRkNDM2MDMyLWQ1MTMtNDU4YS1iNzI3LTZmNTlhMDA2YTIzZiJ9.acf9tiMDFilAlJntro1QcLkw-KubYJaEGNzHQeqRo5Q
}

jwt가 잘 만들어졌는지 확인해보기

아래와 같이 signed jwt가 만들어졌습니다. 해당 jwt가 잘 만들어졌는지 payload를 parsing해서 확인해보도록 하겠습니다.

eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJLYXJvbCIsInN1YiI6IkF1dGgiLCJhdWQiOiJLYXJvbCIsImV4cCI6MTY3NzUwNzMwOSwibmJmIjoxNjc3NDIwOTA5LCJpYXQiOjE2Nzc0MjA5MDksImp0aSI6IjRkNDM2MDMyLWQ1MTMtNDU4YS1iNzI3LTZmNTlhMDA2YTIzZiJ9.acf9tiMDFilAlJntro1QcLkw-KubYJaEGNzHQeqRo5Q

jwt.io 사이트에서 쉽게 파싱이 가능합니다. 파싱을 진행했을 때 아래와 같이 header와 payload가 파싱된것을 확인할 수 있습니다. 등록했던 알고리즘 및 registered fields가 동일하기때문에 정상적으로 잘 만들어졌다고 판단할 수 있을것으로 보입니다.

// header
{
  "alg": "HS256"
}

// payload
{
  "iss": "Karol",
  "sub": "Auth",
  "aud": "Karol",
  "exp": 1677507309,
  "nbf": 1677420909,
  "iat": 1677420909,
  "jti": "4d436032-d513-458a-b727-6f59a006a23f"
}

jwt를 검증하기

jwt를 만들고 parsing까지 완료했으니 이번에는 이번에는 jwt를 검증하는 시간을 가져보도록 하겠습니다. jjwt를 이용하면 아래의 프로세스로 검증이 가능합니다.

  1. Jwts.parserBuilder() method 메서드를 사용해서 JwtParserBuilder 인스턴스를 만듭니다.
  2. JWS 서명을 확인하는 데 사용할 SecretKey 또는 비대칭 공개키를 지정합니다.
  3. build() method 를 호출하면 thread-safe JwtParser가 반환됩니다.
  4. 마지막으로, call the parseClaimsJws(jwtString) method를 호출하면 오리지널 signed JWT가 반환됩니다.
  5. 추가적으로 만약 jwt를 검증하면서 검증에 실패하면 exception이 발생합니다.

테스트코드로 보면 아래와 같습니다.

@Test
public void signed_jwt_verify_with_registered_fields() {
    // given
    String jws = "eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJLYXJvbCIsInN1YiI6IkF1dGgiLCJhdWQiOiJLYXJvbCIsImV4cCI6MTY3NzUwNzMwOSwibmJmIjoxNjc3NDIwOTA5LCJpYXQiOjE2Nzc0MjA5MDksImp0aSI6IjRkNDM2MDMyLWQ1MTMtNDU4YS1iNzI3LTZmNTlhMDA2YTIzZiJ9.acf9tiMDFilAlJntro1QcLkw-KubYJaEGNzHQeqRo5Q";

    // when
    var jwtSubject = Jwts.parserBuilder()
            .setSigningKey(Base64.getDecoder().decode("viS43O0oxOOYZ8Z7m1QVtOyN3o3bEx8X0eQGcRMAxY0="))
            .build();

    var parseClaimsJws = jwtSubject.parseClaimsJws(jws).getBody();

    assertEquals(parseClaimsJws.getIssuer(), "Karol");
    assertEquals(parseClaimsJws.getSubject(), "Auth");
    assertEquals(parseClaimsJws.getAudience(), "Karol");
    assertDoesNotThrow(() -> parseClaimsJws.getExpiration().toInstant().getEpochSecond());
    assertDoesNotThrow(() -> parseClaimsJws.getNotBefore().toInstant().getEpochSecond());
    assertDoesNotThrow(() -> parseClaimsJws.getIssuedAt().toInstant().getEpochSecond());
    assertDoesNotThrow(() -> UUID.fromString(parseClaimsJws.getId()));
}

예외

위의 파싱의 프로세스를 보면 5번의 케이스로 검증에 실패시 exception이 발생된다고 말씀드렸습니다. 검증에는 인증키에 대한 부분도 있지만 exp와 nbf에 대한 검증도 진행합니다. 만약 충족되지 않다면 발생되는 exception은 아래와 같습니다. 아래 exception은 expired를 현재 시간이 넘었을 때 발생하는 에러입니다.

io.jsonwebtoken.ExpiredJwtException: JWT expired at 2023-02-26T16:10:06Z. Current time: 2023-02-26T16:10:06Z, a difference of 700 milliseconds.  Allowed clock skew: 0 milliseconds.

Custom Fields & Complex Custom Fields 생성

추가적으로 jwt의 payload에는 커스텀 필드인 private 필드를 추가할 수 있습니다. 아래 예제 코드를 보도록 하겠습니다.
email이라는 String 필드와 user라는 object 필드를 커스텀 필드로 추가하는 예제입니다. setClaims 메서드를 통해서 추가가 가능합니다.

/**
    * 커스텀(private) 필드가 많은 signed_jwt를 생성 및 검증합니다.
    *
    * 생성되는 payload는 아래와 같습니다.
    * {
    *   "email": "koangho93@naver.com",
    *   "user": {
    *     "nickname": "karol",
    *     "age": 31
    *   }
    * }
    *
    * JacksonSerializer / JacksonDeserializer를 사용합니다.
    * 필드 일부를 propertiy를 파싱하기 위해서는 json value인 key로 가져온 후 objectMapper로 파싱합니다.
    */
@Test
public void signed_jwt_create_with_private_field() {
    // given
    String email = "koangho93@naver.com";
    JwtUserDummy jwtUserDummy = new JwtUserDummy("karol", 31);

    final Key secret = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    final byte[] secretBytes = secret.getEncoded();
    final String base64SecretBytes = Base64.getEncoder().encodeToString(secretBytes);

    // when
    var key = Keys
            .hmacShaKeyFor(secretBytes);

    ObjectMapper objectMapper = new ObjectMapper();

    var jws = Jwts.builder()
            .serializeToJsonWith(new JacksonSerializer(objectMapper))
            .setClaims(Map.of("email", email, "user", jwtUserDummy))
            .signWith(key)
            .compact();

    System.out.println(jws);

    var jwtSubject = Jwts.parserBuilder()
            .deserializeJsonWith(new JacksonDeserializer(objectMapper))
            .setSigningKey(key)
            .build();

    var parseClaimsJws = jwtSubject
            .parseClaimsJws(jws)
            .getBody();

    // then
    assertEquals(parseClaimsJws.get("email", String.class), email);
    assertEquals(
            objectMapper.convertValue(parseClaimsJws.get("user"), JwtUserDummy.class).toString(),
            jwtUserDummy.toString()
    );
}

마무리

오늘은 이렇게 jwt Token을 Java 환경에서 사용하는 방법에 대해서 실습하면서 알아보았습니다.

오늘 위에서 실습한 코드들은 github에 올려두었습니다. 해당 github 코드를 통해 직접 테스트가 가능합니다.

실습 코드 github Link

감사합니다.

참조

[1] https://sabarada.tistory.com/246

[2] https://jwt.io/#debugger-io

[3] https://github.com/jwtk/jjwt

댓글