JSON Web Tokens - jwt.io
を Java で利用する。
利用環境、pom.xml Maven
<dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.11.0</version> </dependency>
JWTの生成
// 有効期間 3分 int minutes = 3; // 秘密鍵 String secret = "123abc"; // 期間設定の為の計算 LocalDateTime nowtime = LocalDateTime.now(); long nowmilisec = nowtime.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(); java.util.Date before = Date.from(ZonedDateTime.of(nowtime, ZoneId.systemDefault()).toInstant()); java.util.Date expiretime = new Date(nowmilisec + (60 * minutes * 1000)); // ヘッダに任意のデータセットは Mapで Map<String, Object> headerClaims = new HashMap<>(); headerClaims.put("ha", "HA"); headerClaims.put("point", 12); // アルゴリズムを秘密鍵で生成 Algorithm algorithm = Algorithm.HMAC256(secret); String token = JWT.create() .withJWTId("222") .withKeyId("aaaa") .withAudience("A", "B", "C") .withIssuer("issuer") .withSubject("subjects") .withIssuedAt(Date.from(ZonedDateTime.of(nowtime, ZoneId.systemDefault()).toInstant())) .withNotBefore(before) .withExpiresAt(expiretime) .withClaim("X-EMAIL", "yip@xxx.xxx.com") .withClaim("X-SIZE", 1024) .withHeader(headerClaims) .sign(algorithm);
ヘッダのJSONが、以下のとおりにできて、
{"kid":"aaaa","ha":"HA","typ":"JWT","alg":"HS256","point":12}
{"aud":["A","B","C"],"sub":"subjects","nbf":1602924613,"iss":"issuer","X-EMAIL":"yip@xxx.xxx.com","exp":1602924793,"iat":1602924613,"X-SIZE":1024,"jti":"222"}
署名が作られて、Base64 エンコードした文字列、String token が作成できます。
これは、それぞれ
String headerDecoded = new String(Base64.getUrlDecoder().decode(JWT.decode(token).getHeader().getBytes()), StandardCharsets.UTF_8);
String payloadDecoded = new String(Base64.getUrlDecoder().decode(JWT.decode(token).getPayload().getBytes()), StandardCharsets.UTF_8);
でJSON文字列化することです。
JWT発行時間検査の為の時刻を生成するのに、com.auth0.jwt は、java.util.Date で
withIssuedAt , withNotBefore , withExpiresAt で生成しなくてはならず、
java.time.LocalDateTime を使用するのスタンダードな今の時代ではちょっと使いにくいです。
次のJWTトークンの読込み&検証でも使用する以下のユーティリティを用意しておくと便利です。
import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.time.ZonedDateTime; import java.util.Locale; import com.auth0.jwt.exceptions.TokenExpiredException; public final class JwtUtil { private JwtUtil() {} public static LocalDateTime getExpiredDate(TokenExpiredException x){ try{ java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH) .parse(x.getMessage().replaceFirst("The Token has expired on ", "")); return Instant.ofEpochMilli(dt.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime(); }catch(ParseException e){ throw new RuntimeException(e); } } public static long getExpireOverTime(TokenExpiredException x) { try{ long epocmili = Instant.now().toEpochMilli(); java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH) .parse(x.getMessage().replaceFirst("The Token has expired on ", "")); return epocmili - dt.getTime(); }catch(ParseException e){ throw new RuntimeException(e); } } public static java.util.Date toDate(LocalDateTime at){ return java.util.Date.from(ZonedDateTime.of(at, ZoneId.systemDefault()).toInstant()); } public static LocalDateTime toLocalDateTime(java.util.Date date) { return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); } }
JWTトークンの読込み&検証
受信したトークン(Base64エンコード済)は、
JWT.require(アルゴリズム).build()
で生成した JWTVerifier で、verify(token)実行することで、署名検証&デコードまでするのですが、
以下のように、ラムダ実行できるものを用意すると便利なはずです。
import java.text.ParseException; import java.text.SimpleDateFormat; import java.time.Instant; import java.time.LocalDateTime; import java.time.ZoneId; import java.util.Locale; import java.util.function.Consumer; import com.auth0.jwt.JWT; import com.auth0.jwt.JWTVerifier; import com.auth0.jwt.algorithms.Algorithm; import com.auth0.jwt.exceptions.TokenExpiredException; import com.auth0.jwt.interfaces.DecodedJWT; public final class JWTVerifyAction{ private Algorithm algorithm; private JWTVerifyAction(Algorithm algorithm){ this.algorithm = algorithm; } public static JWTVerifyAction of(Algorithm algorithm){ return new JWTVerifyAction(algorithm); } public boolean verifyRead(String token, Consumer<DecodedJWT> decoded, Consumer<Throwable> error){ JWTVerifier verifier = JWT.require(algorithm).build(); try{ DecodedJWT jwt = verifier.verify(token); decoded.accept(jwt); return true; }catch(Throwable e){ error.accept(e); } return false; } public LocalDateTime getExpiredDate(TokenExpiredException x){ try{ java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH) .parse(x.getMessage().replaceFirst("The Token has expired on ", "")); return Instant.ofEpochMilli(dt.getTime()).atZone(ZoneId.systemDefault()).toLocalDateTime(); }catch(ParseException e){ throw new RuntimeException(e); } } public long getExpireOverTime(TokenExpiredException x) { try{ long epocmili = Instant.now().toEpochMilli(); java.util.Date dt = new SimpleDateFormat("EEE MMM d HH:mm:ss Z yyyy.", Locale.ENGLISH) .parse(x.getMessage().replaceFirst("The Token has expired on ", "")); return epocmili - dt.getTime(); }catch(ParseException e){ throw new RuntimeException(e); } } }
検証で発生する例外Exception は、
SignatureVerificationException | secretKeyや署名にエラーがある場合 |
AlgorithmMismatchException | 使用 Algorithm が不一致 |
JWTDecodeException | token 改竄エラー |
TokenExpiredException | 有効期限切れエラー |
InvalidClaimException | トークン使用する前にトークン生成時刻になっているエラー |
になります。
使用例
Algorithm algorithm = Algorithm.HMAC256("123abc"); JWTVerifyAction action = JWTVerifyAction.of(algorithm); action.verifyRead(token, d->{ System.out.println("aud = "+ d.getAudience() ); System.out.println("iss = "+ d.getIssuer() ); System.out.println("sub = "+ d.getSubject() ); System.out.println("expired = "+ d.getExpiresAt() ); System.out.println("expired = "+ JwtUtil.toLocalDateTime(d.getExpiresAt()) ); System.out.println("nbf = "+ d.getNotBefore() ); System.out.println("nbf = "+ JwtUtil.toLocalDateTime(d.getNotBefore()) ); System.out.println("iat = "+ d.getIssuedAt() ); System.out.println("iat = "+ JwtUtil.toLocalDateTime(d.getIssuedAt()) ); System.out.println("X-EMAI = "+ d.getClaim("X-EMAIL").asString() ); System.out.println("X-SIZE = "+ d.getClaim("X-SIZE").asInt() ); System.out.println("jti = "+ d.getId() ); System.out.println("Algorithm = "+ d.getAlgorithm() ); System.out.println("header typ = "+ d.getType() ); System.out.println("header kid = "+ d.getKeyId() ); System.out.println("headerClaim ha = "+ d.getHeaderClaim("ha").asString() ); System.out.println("headerClaim point = "+ d.getHeaderClaim("point").asInt() ); System.out.println("signature = "+ d.getSignature() ); System.out.println("payload = "+ d.getPayload() ); System.out.println("header = "+ d.getHeader() ); String payloadJson = new String(Base64.getUrlDecoder().decode(d.getPayload().getBytes()), StandardCharsets.UTF_8); System.out.println(payloadJson); String headerJson = new String(Base64.getUrlDecoder().decode(d.getHeader().getBytes()), StandardCharsets.UTF_8); System.out.println(headerJson); }, e->{ if (e instanceof TokenExpiredException){ LocalDateTime expAt = JwtUtil.getExpiredDate((TokenExpiredException) e); System.out.print("TokenExpiredException !! expAt = "+expAt); long overtime = JwtUtil.getExpireOverTime((TokenExpiredException) e); System.out.println(" over : " + overtime + " msec"); }else if(e instanceof SignatureVerificationException){ System.out.print("SignatureVerificationException !! secretKey or 署名にエラー "); }else if(e instanceof AlgorithmMismatchException){ System.out.print("AlgorithmMismatchException !! アルゴリズムが一致しない "); }else if(e instanceof JWTDecodeException){ System.out.print("JWTDecodeException !! トークンが改竄 "); }else if(e instanceof InvalidClaimException){ System.out.print("InvalidClaimException !! nbf > iat のトークン使用する前にトークン生成時刻になっているエラー"); }else{ System.out.println("その他のエラー:" + e.getMessage()); } // TODO } );
com.auth0.jwt は、JSON を処理するのに、Jackson を使用しています。
使用する依存ライブラリは気をつけたほうが良いでしょう。