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

pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
build.gradle
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.issuersmallrye.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) 私钥以及对称密钥对声明进行签名。 ES256HS256 分别是 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-ESA256KW 分别是 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

smallrye.jwt.sign.key.location

none

用于在 sign()innerSign() 方法不带参数时签名声明的私钥的位置。

smallrye.jwt.sign.key

none

用于在 sign()innerSign() 方法不带参数时签名声明的关键值。

smallrye.jwt.sign.key.id

none

仅在使用 JWK 密钥时检查的签名密钥标识符。

smallrye.jwt.encrypt.key.location

none

用于在不带参数的 encrypt() 方法调用时加密声明或内部 JWT 的公钥位置。

smallrye.jwt.sign.relax-key-validation

false

放松对签名密钥的验证。

smallrye.jwt.encrypt.key

none

加密声明或内部 JWT 时(无参数 encrypt() 方法),使用此关键值。

smallrye.jwt.encrypt.key.id

none

仅在使用 JWK 密钥时检查的加密密钥标识符。

smallrye.jwt.encrypt.relax-key-validation

false

放松对加密密钥的验证。

smallrye.jwt.new-token.signature-algorithm

RS256

签名算法。如果 JWT 签名构建器尚未设置签名算法,将检查此属性。

smallrye.jwt.new-token.key-encryption-algorithm

RSA-OAEP

密钥加密算法。如果 JWT 加密构建器尚未设置密钥加密算法,将检查此属性。

smallrye.jwt.new-token.content-encryption-algorithm

A256GCM

内容加密算法。如果 JWT 加密构建器尚未设置内容加密算法,将检查此属性。

smallrye.jwt.new-token.lifespan

300

按秒计算的令牌寿命,用于计算 exp (到期)声明值(如果此声明尚未设置)。

smallrye.jwt.new-token.issuer

none

令牌颁发者,可用于设置 iss (颁发者)声明值(如果此声明尚未设置)。

smallrye.jwt.new-token.audience

none

令牌的目标受众,可用于设置 aud (目标受众)声明值(如果此声明尚未设置)。

smallrye.jwt.new-token.override-matching-claims

false

将此属性设置为 true 用于 smallrye.jwt.new-token.issuersmallrye.jwt.new-token.audience 值,可覆盖已初始化的 iss (颁发者)和 aud (目标受众)声明。

smallrye.jwt.keystore.type

JKS

如果 smallrye.jwt.sign.key.locationsmallrye.jwt.encrypt.key.location 或这两个属性指向 KeyStore 文件,则可以使用此属性来自定义密钥库类型。如果没有设置,则在默认为 JKS 之前会检查文件名以确定密钥库类型。

smallrye.jwt.keystore.provider

如果 smallrye.jwt.sign.key.locationsmallrye.jwt.encrypt.key.location 指向 KeyStore 文件,则可以使用此属性来自定义一个 KeyStore 提供程序。

smallrye.jwt.keystore.password

密钥库密码。如果 smallrye.jwt.sign.key.locationsmallrye.jwt.encrypt.key.location 指向 KeyStore 文件,则必须设置此属性。

smallrye.jwt.keystore.encrypt.key.alias

必须设置此属性,以便识别一个公共加密密钥,此密钥将从 KeyStore 中提取,方法是从 smallrye.jwt.encrypt.key.location 指向 KeyStore 文件的匹配证书中提取。

smallrye.jwt.keystore.sign.key.alias

如果 smallrye.jwt.sign.key.location 指向 KeyStore 文件,则必需设置此属性以标识私有签名密钥。

smallrye.jwt.keystore.sign.key.password

如果私有签名密钥在 smallrye.jwt.keystore.passwordKeyStore 中的密码不同于 smallrye.jwt.sign.key.location 指向 KeyStore 文件时,可以设置此属性。