Len's Study-Log

集中一点,登峰造极!

0%

Java JWT译文

JSON Web Token (JWT) - RFC 7519 的 Java 实现。

此库需要Java 8或更高版本。支持Java 7的最后一个版本为3.11.0。

安装

本库可以在 Maven 和 Bintray 上找到,Java API 文档在这里

Maven

1
2
3
4
5
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.0</version>
</dependency>

Gradle

1
implementation 'com.auth0:java-jwt:3.19.0'

可用算法

本库可以使用以下算法验证和签署 JWT:

JWS Algorithm Description
HS256 HMAC256 HMAC with SHA-256
HS384 HMAC384 HMAC with SHA-384
HS512 HMAC512 HMAC with SHA-512
RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256
RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384
RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512
ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
ES512 ECDSA512 ECDSA with curve P-521 and SHA-512

⚠️ Note - ECDSA with curve secp256k1 and SHA-256 will not be supported for Java 15+ by this library since it has been (disabled in Java 15)[https://www.oracle.com/java/technologies/javase/15-relnote-issues.html#JDK-8237219]

用法

选择算法

算法定义了 Token 的签名和验证方式。在 HMAC 算法中可以使用 secret 的原始值实例化,在 RSA 和 ECDSA 算法中可以使用密钥对或 KeyProvider。一旦创建,该实例可以重复使用于 Token 签名和验证等操作。

当使用 RSA 或 ECDSA 算法时,您只需要签署 JWTs,就可以通过传递空值来避免指定公钥。当您只需要验证 JWTs 时,也可以使用 Private Key 完成同样的操作。

使用静态 Secrets 或者 keys:

1
2
3
4
5
6
7
//HMAC
Algorithm algorithmHS = Algorithm.HMAC256("secret");

//RSA
RSAPublicKey publicKey = //Get the key instance
RSAPrivateKey privateKey = //Get the key instance
Algorithm algorithmRS = Algorithm.RSA256(publicKey, privateKey);

注:怎么获取或读取密钥不在本库介绍范围之内,想知道如何实现?可以参考这里

HMAC key 的长度和安全性

当使用基于哈希的消息认证码时,例如,HS256或HS512,为了遵守 JSON Web 算法(JWA)规范(RFC7518)的严格要求,必须使用与输出哈希大小具有相同(或更大)位长的密钥。这是为了避免削弱认证码的安全强度(参见NIST Recendations Nist SP 800-117)。例如,当使用HMAC256时,秘密密钥长度必须至少为256位。

使用 KeyProvider:

通过使用 KeyProvider,你可以在运行时更改签名 Token 的键,或者用 RSA 或 ECDSA 算法 去签名一个新 token。

这是通过实现 RSAKeyProviderECDSAKeyProvider 方法做到的:

  • getPublicKeyById(String kid): 在 Token 签名验证期间调用,返回用于验证 Token 的 key。如果轮值 key 被使用过了,它可以使用该 kid 获取到正确的轮值 key(或者只是返回相同的 key),类似于 JWK
  • getPrivateKey(): 在 Token 签名期间调用,返回用于签署JWT的密钥。
  • getPrivateKeyId(): 在 Token 签名期间调用,返回 key(同 getPrivateKey() 返回的 key) 的 id ,该值是 JWTCreator.Builder#withKeyId(String) 方法中设置的值的首选。如果你不需要设置 kid ,请避免使用 KeyProvider 去实例化一个算法。

接下来的例子展示了如何和 JwkStore 一起使用, 这是一个虚拟的JWK 实现。有关 JWKS 如何使用轮值 key ,请看 jwks-rsa-java 库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
final JwkStore jwkStore = new JwkStore("{JWKS_FILE_HOST}");
final RSAPrivateKey privateKey = //Get the key instance
final String privateKeyId = //Create an Id for the above key

RSAKeyProvider keyProvider = new RSAKeyProvider() {
@Override
public RSAPublicKey getPublicKeyById(String kid) {
//Received 'kid' value might be null if it wasn't defined in the Token's header
RSAPublicKey publicKey = jwkStore.get(kid);
return (RSAPublicKey) publicKey;
}

@Override
public RSAPrivateKey getPrivateKey() {
return privateKey;
}

@Override
public String getPrivateKeyId() {
return privateKeyId;
}
};

Algorithm algorithm = Algorithm.RSA256(keyProvider);
//Use the Algorithm to create and verify JWTs.

创建和签名 Token

首先,调用 JWT.create() 方法并且传入 Algorithm 类型参数创建一个 JWTCreator 实例。然后,使用Builder定义你的 Token 需要的定制化的Claims。最后,通过调用 sign()方法获取 String 类型的 token。

  • 使用 HS256 的示例
1
2
3
4
5
6
7
8
try {
Algorithm algorithm = Algorithm.HMAC256("secret");
String token = JWT.create()
.withIssuer("auth0")
.sign(algorithm);
} catch (JWTCreationException exception){
//Invalid Signing configuration / Couldn't convert Claims.
}
  • 使用 RS256 的示例
1
2
3
4
5
6
7
8
9
RSAPrivateKey privateKey = //Get the private key instance
try {
Algorithm algorithm = Algorithm.RSA256(null, privateKey); // only the private key is used for signing
String token = JWT.create()
.withIssuer("auth0")
.sign(algorithm);
} catch (JWTCreationException exception){
//Invalid Signing configuration / Couldn't convert Claims.
}

如果一个 Claim 无法被转换成 JSON,或者签名过程中使用的 Key 无效,会抛出 JWTCreationException 异常。

验证 Token

首先,调用 JWT.require() 方法并且传入 Algorithm 类型参数 获得 JWTVerifier,如果你要求 Token 有特定的 Claim 值,可以使用Builder 去定义他们。方法 build() 返回的实例是可以重复使用的,所以定义一次之后就可以验证不同的 token。最后,调用 verifier.verify() 方法并且传入参数 token。

  • 使用 HS256 的示例
1
2
3
4
5
6
7
8
9
10
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
Algorithm algorithm = Algorithm.HMAC256("secret");
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth0")
.build(); //Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
} catch (JWTVerificationException exception){
//Invalid signature/claims
}
  • 使用 RS256 的示例
1
2
3
4
5
6
7
8
9
10
11
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
RSAPublicKey publicKey = //Get the key instance
try {
Algorithm algorithm = Algorithm.RSA256(publicKey, null); // only the public key is used during verification
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("auth0")
.build(); //Reusable verifier instance
DecodedJWT jwt = verifier.verify(token);
} catch (JWTVerificationException exception){
//Invalid signature/claims
}

如果 Token 签名错误,或者 Claim 不符合要求,会抛出 JWTVerificationException 异常。

有效期验证

JWT Token 可能包含日期数值字段,可以通过以下方式验证:

  • Token 在过去日期发布 "iat" < TODAY

  • Token 尚未过期 "exp" > TODAY

  • Token 目前可用 "nbf" < TODAY

验证令牌时,将自动发生时间验证,从而在值无效时抛出 jwtverificationException 异常。如果没有前缀属性,就会忽略这种验证。

要指定一个空余值(leeway window)让 Token 被认为有效,可以通过流式调用 JWTVerifieracceptLeeway() 方法并传入一个(正整数)秒数值。这适用于上面列出的每一项。

1
2
3
JWTVerifier verifier = JWT.require(algorithm)
.acceptLeeway(1) // 1 sec for nbf, iat and exp
.build();

你也可以为给定日期的 Claim 指定一个自定义值。并且覆盖该 Claim 的默认值。

1
2
3
4
JWTVerifier verifier = JWT.require(algorithm)
.acceptLeeway(1) //1 sec for nbf and iat
.acceptExpiresAt(5) //5 secs for exp
.build();

如果你需要在你的 lib/app 中测试此行为,可以将 Verification 实例转换为 BaseVerification ,然后就可以使用verification.build() 方法接收一个自动以的 Clock入参。例如:

1
2
3
4
5
BaseVerification verification = (BaseVerification) JWT.require(algorithm)
.acceptLeeway(1)
.acceptExpiresAt(5);
Clock clock = new CustomClock(); //Must implement Clock interface
JWTVerifier verifier = verification.build(clock);

解码 Token

1
2
3
4
5
6
String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXUyJ9.eyJpc3MiOiJhdXRoMCJ9.AbIJTDMFc7yUa5MhvcP03nJPyCPzZtQcGEp-zWfOkEE";
try {
DecodedJWT jwt = JWT.decode(token);
} catch (JWTDecodeException exception){
//Invalid token
}

如果 Token 不符合语法,或者 header/payload 不是 JSON,会抛出 JWTDecodeException 异常。

Header Claims

Algorithm (“alg”)

Returns the Algorithm value or null if it’s not defined in the Header.

返回设置的算法值,如果没在 Header 定义,则返回 null。

1
String algorithm = jwt.getAlgorithm();

Type (“typ”)

返回设置的类型,如果没在 Header 定义,则返回 null。

1
String type = jwt.getType();

Content Type (“cty”)

返回设置的内容,如果没在 Header 定义,则返回 null。

1
String contentType = jwt.getContentType();

Key Id (“kid”)

返回设置的Key Id,如果没在 Header 定义,则返回 null。

1
String keyId = jwt.getKeyId();

私有 Claims

在 Token 的 Header 部分定义的额外的 Claim,可以通过调用 getHeaderClaim()方法并传入 Claim 名获取。该方法返回的值不会是 null,所以你可以通过 claim.isNull() 去检查一下。

1
Claim claim = jwt.getHeaderClaim("owner");

当使用 JWT.create() 创建一个 Token 时,可以通过 withHeader() 方法(传入一个 claim 的Map集)指定 header 中的 Claims。

1
2
3
4
5
Map<String, Object> headerClaims = new HashMap();
headerClaims.put("owner", "auth0");
String token = JWT.create()
.withHeader(headerClaims)
.sign(algorithm);

签名之后,algtyp 值会一直包含在 Header 中。

载入 Claims

Issuer (“iss”)

返回发布人,如果没在 Payload 定义,则返回 null。

1
String issuer = jwt.getIssuer();

Subject (“sub”)

返回主题,如果没在 Payload 定义,则返回 null。

1
String subject = jwt.getSubject();

Audience (“aud”)

返回观众,如果没在 Payload 定义,则返回 null。

1
List<String> audience = jwt.getAudience();

Expiration Time (“exp”)

返回到期时间,如果没在 Payload 定义,则返回 null。

1
Date expiresAt = jwt.getExpiresAt();

Not Before (“nbf”)

返回 Not Before,如果没在 Payload 定义,则返回 null。

1
Date notBefore = jwt.getNotBefore();

Issued At (“iat”)

返回发布时间,如果没在 Payload 定义,则返回 null。

1
Date issuedAt = jwt.getIssuedAt();

JWT ID (“jti”)

返回 JWT ID,如果没在 Payload 定义,则返回 null。

1
String id = jwt.getId();

Private Claims

可以通过 getClaims() 或者 getClaim() 获取定义在 Token Payload 中的额外 Claims。两个方法返回值不为空,所以可通过 claim.isNull() 去检查一下。

1
2
Map<String, Claim> claims = jwt.getClaims();    //Key is the Claim name
Claim claim = claims.get("isAdmin");

也可以这样写:

1
Claim claim = jwt.getClaim("isAdmin");

当使用 JWT.create() 创建一个 Token 时,可以通过 withClaim() 方法(传入一个 claim 的Map集)指定一个自定义的 Claim。

1
2
3
4
String token = JWT.create()
.withClaim("name", 123)
.withArrayClaim("array", new Integer[]{1, 2, 3})
.sign(algorithm);

你也可以调用 withPayload() 方法并且传入 Claim 的 Map集来创建一个 JWT:

1
2
3
4
5
Map<String, Object> payloadClaims = new HashMap<>();
payloadClaims.put("@context", "https://auth0.com/");
String token = JWT.create()
.withPayload(payloadClaims)
.sign(algorithm);

你也可以通过 JWT.require() 调用方法 withClaim() 来验证自定义的 Claims :

1
2
3
4
5
JWTVerifier verifier = JWT.require(algorithm)
.withClaim("name", 123)
.withArrayClaim("array", 1, 2, 3)
.build();
DecodedJWT jwt = verifier.verify("my.jwt.token");

目前支持的自定义JWT Claim 创建和验证的类型是:Boolean,Integer,Double,String,字符串和整数的日期和数组。

Claim 类型

Claim 类是 Claim 值的包装,它允许你将 Claim 作为不同的类型。对此有帮助的如下:

Primitives

  • asBoolean(): 返回 Boolean 值或者 null.
  • asInt(): 返回 Integer 值或者 null.
  • asDouble(): 返回 Double 值或者 null.
  • asLong(): 返回 Long 值或者 null.
  • asString(): 返回 String 值或者 null.
  • asDate(): 返回 Date 值或者 null。这里的返回值一定是数值类型的日期 (Unix Epoch/Timestamp). 请注意,JWT Standard 指定的是所有NumericDate值的单位都是秒。

自定义类和集合

要获取集合类型的 Claim,你需要提供一个类型值类让它转换:

  • as(class): 返回被解析为指定类型的值。对于集合,应使用asArrayasList 方法。
  • asMap(): 返回被解析为 Map 的值.
  • asArray(class): 返回被解析为指定类型的数组。如果值不是 JSON 数组,则返回 null.
  • asList(class): 返回被解析为指定类型的 List, 如果值不是 JSON 数组,则返回 null.

如果值不能转换为给定的类型,会抛出 JWTDecodeException 异常。


原文链接:https://github.com/auth0/java-jwt#algorithm-alg