Build, Sign and Encrypt JSON Web Tokens
据 RFC7519所说,JSON Web 令牌 (JWT) 是一种紧凑、URL 安全的方法,用于表示作为 JSON 对象编码的声明,该对象用作 JSON Web 签名 (JWS) 结构的有效负载或 JSON Web 加密 (JWE) 结构的纯文本,允许使用消息认证代码 (MAC) 和/或加密对声明进行数字签名或完整性保护。 最常使用对声明进行签名来保护声明。当今所说的 JWT 令牌通常是通过使用 JSON Web Signature规范中描述的步骤以 JSON 格式对声明进行签名来生成的。 但是,当声明很敏感时,可以通过按照 JSON Web Encryption 规范中介绍的步骤来生成带有加密声明的 JWT 令牌,以保证其机密性。 最后,可以通过首先对它们进行签名,然后再对嵌套的 JWT 令牌进行加密,进一步加强声明的机密性和完整性。 SmallRye JWT 构建提供了一个 API,可使用所有这些选项来保护 JWT 声明。 Jose4J 在内部用于支持此 API。
Dependency
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
implementation("io.quarkus:quarkus-smallrye-jwt-build")
请注意,您可以在不创建由 quarkus-smallrye-jwt
支持的 MicroProfile JWT 终结点的情况下使用 Smallrye JWT 构建 API。如果 MP JWT 终结点不需要生成 JWT 令牌,它还可以从 quarkus-smallrye-jwt
中排除。
Create JwtClaimsBuilder and set the claims
第一步是使用以下选项之一初始化一个 JwtClaimsBuilder
,并为它添加一些声明:
import java.util.Collections;
import jakarta.json.Json;
import jakarta.json.JsonObject;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.build.JwtClaimsBuilder;
import org.eclipse.microprofile.jwt.JsonWebToken;
...
// Create an empty builder and add some claims
JwtClaimsBuilder builder1 = Jwt.claims();
builder1.claim("customClaim", "custom-value").issuer("https://issuer.org");
// Or start typing the claims immediately:
// JwtClaimsBuilder builder1 = Jwt.upn("Alice");
// Builder created from the existing claims
JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json");
// Builder created from a map of claims
JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value"));
// Builder created from JsonObject
JsonObject userName = Json.createObjectBuilder().add("username", "Alice").build();
JsonObject userAddress = Json.createObjectBuilder().add("city", "someCity").add("street", "someStreet").build();
JsonObject json = Json.createObjectBuilder(userName).add("address", userAddress).build();
JwtClaimsBuilder builder4 = Jwt.claims(json);
// Builder created from JsonWebToken
@Inject JsonWebToken token;
JwtClaimsBuilder builder5 = Jwt.claims(token);
此 API 是流畅的,因此构建器初始化可以作为流畅 API 序列的一部分完成。
构建器还会将 iat
(颁发时间)设置为当前时间,将 exp
(过期时间)设置为当前时间的 5 分钟后(可以通过 smallrye.jwt.new-token.lifespan
属性进行自定义),以及将 jti
(唯一令牌标识符)声明设置为尚未设置的声明。
还可以配置 smallrye.jwt.new-token.issuer
和 smallrye.jwt.new-token.audience
属性,并使用构建器 API 直接跳过设置颁发者和受众。
下一步是决定如何保护声明。
Sign the claims
声明可以在 JSON Web Signature
标头设置后立即签名或在设置后签名:
import io.smallrye.jwt.build.Jwt;
...
// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property.
// No 'jws()' transition is necessary. Default algorithm is RS256.
String jwt1 = Jwt.claims("/tokenClaims.json").sign();
// Set the headers and sign the claims with an RSA private key loaded in the code (the implementation of this method is omitted).
// Note a 'jws()' transition to a 'JwtSignatureBuilder', Default algorithm is RS256.
String jwt2 = Jwt.claims("/tokenClaims.json").jws().keyId("kid1").header("custom-header", "custom-value").sign(getPrivateKey());
请注意, alg
(算法)标头默认设置为 RS256
。如果使用包含 kid
属性的单个 JSON Web Key (JWK),则无需设置签名密钥标识符 (kid
标头)。
可以使用 RSA 和椭圆曲线 (EC) 私钥以及对称密钥对声明进行签名。 ES256
和 HS256
分别是 EC 私钥和对称密钥算法的默认算法。
您可以自定义签名算法,例如:
import io.smallrye.jwt.SignatureAlgorithm;
import io.smallrye.jwt.build.Jwt;
// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. Algorithm is PS256.
String jwt = Jwt.upn("Alice").jws().algorithm(SignatureAlgorithm.PS256).sign();
或者,您可以使用 smallrye.jwt.new-token.signature-algorithm
属性:
smallrye.jwt.new-token.signature-algorithm=PS256
并编写一个更简单的 API 序列:
import io.smallrye.jwt.build.Jwt;
// Sign the claims using an RSA private key loaded from the location set with a 'smallrye.jwt.sign.key.location' property. Algorithm is PS256.
String jwt = Jwt.upn("Alice").sign();
请注意,第 sign
步可以与第 encrypt 步结合以生成 inner-signed and encrypted
令牌,请参见 Sign the claims and encrypt the nested JWT token 部分。
Encrypt the claims
声明可以在 JSON Web Encryption
标头设置后立即加密或在设置后加密,方式可以与签名一样。唯一的细微差别是,加密声明总是需要一个 jwe()
JwtEncryptionBuilder
过渡,因为该 API 已针对支持签署和声明的内部签名进行了优化。
import io.smallrye.jwt.build.Jwt;
...
// Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property. Default key encryption algorithm is RSA-OAEP.
String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt();
// Set the headers and encrypt the claims with an RSA public key loaded in the code (the implementation of this method is omitted). Default key encryption algorithm is A256KW.
String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getSecretKey());
请注意, alg
(密钥管理算法)标头默认设置为 RSA-OAEP
,而 enc
(内容加密标头)默认设置为 A256GCM
。
可以使用 RSA 和椭圆曲线 (EC) 公钥以及对称密钥对声明进行加密。 ECDH-ES
和 A256KW
分别是 EC 公钥和对称密钥加密算法的默认算法。
在创建经过加密令牌时,需执行两种加密操作:
1) 使用 API 提供的密钥加密生成的内容加密密钥,使用密钥加密算法,如 RSA-OAEP`2) 使用生成的内容加密密钥加密声明,使用内容加密算法,如 `A256GCM
。
您可以自定义密钥和内容加密算法,例如:
import io.smallrye.jwt.KeyEncryptionAlgorithm;
import io.smallrye.jwt.ContentEncryptionAlgorithm;
import io.smallrye.jwt.build.Jwt;
// Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property.
// Key encryption algorithm is RSA-OAEP-256, content encryption algorithm is A256CBC-HS512.
String jwt = Jwt.subject("Bob").jwe()
.keyAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP_256)
.contentAlgorithm(ContentEncryptionAlgorithm.A256CBC_HS512)
.encrypt();
或者,您可以使用 `smallrye.jwt.new-token.key-encryption-algorithm`和 `smallrye.jwt.new-token.content-encryption-algorithm`属性自定义密钥和内容加密算法:
smallrye.jwt.new-token.key-encryption-algorithm=RSA-OAEP-256
smallrye.jwt.new-token.content-encryption-algorithm=A256CBC-HS512
并编写一个更简单的 API 序列:
import io.smallrye.jwt.build.Jwt;
// Encrypt the claims using an RSA public key loaded from the location set with a 'smallrye.jwt.encrypt.key.location' property.
// Key encryption algorithm is RSA-OAEP-256, content encryption algorithm is A256CBC-HS512.
String jwt = Jwt.subject("Bob").encrypt();
请注意,当使用公钥 RSA 或 EC 对令牌直接进行加密时,不可能验证发送此令牌的一方。因此,应优先使用机密密钥对令牌进行直接加密,例如,在使用 JWT 作为 Cookie 时,由 Quarkus 端点管理机密密钥,并且只有该端点既是经过加密令牌的生产者,又是消费者。
如果您希望使用 RSA 或 EC 公钥加密令牌,则建议先对令牌进行签名(如果签名密钥可用),请参阅下一个 Sign the claims and encrypt the nested JWT token部分。
Sign the claims and encrypt the nested JWT token
声明可以先进行签名,然后通过组合签名和加密步骤对嵌套 JWT 令牌进行加密。
import io.smallrye.jwt.build.Jwt;
...
// Sign the claims and encrypt the nested token using the private and public keys loaded from the locations set with the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties respectively. Signature algorithm is RS256, key encryption algorithm is RSA-OAEP-256.
String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt();
Fast JWT Generation
如果设置了 `smallrye.jwt.sign.key.location`或/和 `smallrye.jwt.encrypt.key.location`属性,那么可以通一个调用保护现有声明(资源、映射、JsonObjects):
// More compact than Jwt.claims("/claims.json").sign();
Jwt.sign("/claims.json");
// More compact than Jwt.claims("/claims.json").jwe().encrypt();
Jwt.encrypt("/claims.json");
// More compact than Jwt.claims("/claims.json").innerSign().encrypt();
Jwt.signAndEncrypt("/claims.json");
如上所述,在需要时,将添加 iat
(已发布)、exp
(已过期)、jti
(令牌标识符)、iss
(发行者)和 aud
(受众)声明。
Dealing with the keys
smallrye.jwt.sign.key.location`和 `smallrye.jwt.encrypt.key.location`属性可用于指向签名密钥和加密密钥位置。这些密钥可以位于本地文件系统、类路径上,也可以从远程端点获取,并且可以为 `PEM`或 `JSON Web Key
(JWK
) 格式。例如:
smallrye.jwt.sign.key.location=privateKey.pem
smallrye.jwt.encrypt.key.location=publicKey.pem
您还可以使用 MicroProfile `ConfigSource`从外部服务(如 HashiCorp Vault或其他机密管理器)中获取密钥,并使用 `smallrye.jwt.sign.key`和 `smallrye.jwt.encrypt.key`属性进行替代:
smallrye.jwt.sign.key=${private.key.from.vault}
smallrye.jwt.encrypt.key=${public.key.from.vault}
其中:`private.key.from.vault`和 `public.key.from.vault`是自定义 `ConfigSource`提供的 `PEM`或 `JWK`格式密钥值。`smallrye.jwt.sign.key`和 `smallrye.jwt.encrypt.key`也可仅仅包含 Base64 编码的私钥或公钥值。
但是,请注意,不建议在配置中直接内联私钥。仅当您需要从远程机密管理器中获取签名密钥值时,再使用 `smallrye.jwt.sign.key`属性。
密钥还可以由构建令牌的代码加载并提供给 JWT 构建 API。
如果您需要使用对称机密密钥对令牌进行签名和/或加密,那么请考虑使用 `io.smallrye.jwt.util.KeyUtils`生成所需长度的 SecretKey。
例如,使用 HS512`算法 (`512/8
) 进行签名需要一个 64 字节的密钥,而使用 A256KW`算法 (`256/8
) 加密内容加密密钥需要一个 32 字节的密钥:
import javax.crypto.SecretKey;
import io.smallrye.jwt.KeyEncryptionAlgorithm;
import io.smallrye.jwt.SignatureAlgorithm;
import io.smallrye.jwt.build.Jwt;
import io.smallrye.jwt.util.KeyUtils;
SecretKey signingKey = KeyUtils.generateSecretKey(SignatureAlgorithm.HS512);
SecretKey encryptionKey = KeyUtils.generateSecretKey(KeyEncryptionAlgorithm.A256KW);
String jwt = Jwt.claim("sensitiveClaim", getSensitiveClaim()).innerSign(signingKey).encrypt(encryptionKey);
您还可以考虑使用 JSON Web Key
(JWK) 或 JSON Web Key Set
(JWK 集) 格式将机密密钥存储在安全的文件系统上,并且使用 `smallrye.jwt.sign.key.location`或 `smallrye.jwt.encrypt.key.location`属性引用,例如:
{
"kty":"oct",
"kid":"secretKey",
"k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"
}
或
{
"keys": [
{
"kty":"oct",
"kid":"secretKey1",
"k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"
},
{
"kty":"oct",
"kid":"secretKey2",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
]
}
io.smallrye.jwt.util.KeyUtils`还可以用于生成一对非对称 RSA 或 EC 密钥。这些密钥可以使用 `JWK
、`JWK Set`或 `PEM`格式存储。
SmallRye JWT Builder configuration
SmallRye JWT 支持以下属性,可用于自定义对声明进行签名和/或加密的方式:
Property Name | Default | Description |
---|---|---|
|
|
用于在 |
|
|
用于在 |
|
|
仅在使用 JWK 密钥时检查的签名密钥标识符。 |
|
|
用于在不带参数的 |
|
|
放松对签名密钥的验证。 |
|
|
加密声明或内部 JWT 时(无参数 encrypt() 方法),使用此关键值。 |
|
|
仅在使用 JWK 密钥时检查的加密密钥标识符。 |
|
|
放松对加密密钥的验证。 |
|
|
签名算法。如果 JWT 签名构建器尚未设置签名算法,将检查此属性。 |
|
|
密钥加密算法。如果 JWT 加密构建器尚未设置密钥加密算法,将检查此属性。 |
|
|
内容加密算法。如果 JWT 加密构建器尚未设置内容加密算法,将检查此属性。 |
|
|
按秒计算的令牌寿命,用于计算 |
|
|
令牌颁发者,可用于设置 |
|
|
令牌的目标受众,可用于设置 |
|
|
将此属性设置为 |
|
|
如果 |
|
如果 |
|
|
密钥库密码。如果 |
|
|
必须设置此属性,以便识别一个公共加密密钥,此密钥将从 |
|
|
如果 |
|
|
如果私有签名密钥在 |