Build, Sign and Encrypt JSON Web Tokens

RFC7519所说,JSON Web 令牌 (JWT) 是一种紧凑、URL 安全的方法,用于表示作为 JSON 对象编码的声明,该对象用作 JSON Web 签名 (JWS) 结构的有效负载或 JSON Web 加密 (JWE) 结构的纯文本,允许使用消息认证代码 (MAC) 和/或加密对声明进行数字签名或完整性保护。

According to RFC7519, JSON Web Token (JWT) is a compact, URL-safe means of representing claims which are encoded as a JSON object that is used as the payload of a JSON Web Signature (JWS) structure or as the plaintext of a JSON Web Encryption (JWE) structure, enabling the claims to be digitally signed or integrity protected with a Message Authentication Code(MAC) and/or encrypted.

最常使用对声明进行签名来保护声明。当今所说的 JWT 令牌通常是通过使用 JSON Web Signature规范中描述的步骤以 JSON 格式对声明进行签名来生成的。

Signing the claims is used most often to secure the claims. What is known today as a JWT token is typically produced by signing the claims in a JSON format using the steps described in the JSON Web Signature specification.

但是,当声明很敏感时,可以通过按照 JSON Web Encryption 规范中介绍的步骤来生成带有加密声明的 JWT 令牌,以保证其机密性。

However, when the claims are sensitive, their confidentiality can be guaranteed by following the steps described in the JSON Web Encryption specification to produce a JWT token with the encrypted claims.

最后,可以通过首先对它们进行签名,然后再对嵌套的 JWT 令牌进行加密,进一步加强声明的机密性和完整性。

Finally, both the confidentiality and integrity of the claims can be further enforced by signing them first and then encrypting the nested JWT token.

SmallRye JWT 构建提供了一个 API,可使用所有这些选项来保护 JWT 声明。 Jose4J 在内部用于支持此 API。

SmallRye JWT Build provides an API for securing JWT claims using all of these options. Jose4J is used internally to support this 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 中排除。

Note you can use Smallrye JWT Build API without having to create MicroProfile JWT endpoints supported by quarkus-smallrye-jwt. It can also be excluded from quarkus-smallrye-jwt if MP JWT endpoints do not need to generate JWT tokens.

Create JwtClaimsBuilder and set the claims

第一步是使用以下选项之一初始化一个 JwtClaimsBuilder,并为它添加一些声明:

The first step is to initialize a JwtClaimsBuilder using one of the options below and add some claims to it:

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 序列的一部分完成。

The API is fluent so the builder initialization can be done as part of the fluent API sequence.

构建器还会将 iat (颁发时间)设置为当前时间,将 exp (过期时间)设置为当前时间的 5 分钟后(可以通过 smallrye.jwt.new-token.lifespan 属性进行自定义),以及将 jti (唯一令牌标识符)声明设置为尚未设置的声明。

The builder will also set iat (issued at) to the current time, exp (expires at) to 5 minutes away from the current time (it can be customized with the smallrye.jwt.new-token.lifespan property) and jti (unique token identifier) claims if they have not already been set.

还可以配置 smallrye.jwt.new-token.issuersmallrye.jwt.new-token.audience 属性,并使用构建器 API 直接跳过设置颁发者和受众。

One can also configure smallrye.jwt.new-token.issuer and smallrye.jwt.new-token.audience properties and skip setting the issuer and audience directly with the builder API.

下一步是决定如何保护声明。

The next step is to decide how to secure the claims.

Sign the claims

声明可以在 JSON Web Signature 标头设置后立即签名或在设置后签名:

The claims can be signed immediately or after the JSON Web Signature headers have been set:

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 标头)。

Note the alg (algorithm) header is set to RS256 by default. Signing key identifier (kid header) does not have to be set if a single JSON Web Key (JWK) containing a kid property is used.

可以使用 RSA 和椭圆曲线 (EC) 私钥以及对称密钥对声明进行签名。 ES256HS256 分别是 EC 私钥和对称密钥算法的默认算法。

RSA and Elliptic Curve (EC) private keys as well as symmetric secret keys can be used to sign the claims. ES256 and HS256 are the default algorithms for EC private and symmetric key algorithms respectively.

您可以自定义签名算法,例如:

You can customize the signature algorithm, for example:

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 属性:

Alternatively you can use a smallrye.jwt.new-token.signature-algorithm property:

smallrye.jwt.new-token.signature-algorithm=PS256

并编写一个更简单的 API 序列:

and write a simpler API sequence:

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 部分。

Note the sign step can be combined with the encrypt-claims step to produce inner-signed and encrypted tokens, see innersign-encrypt-claims section.

Encrypt the claims

声明可以在 JSON Web Encryption 标头设置后立即加密或在设置后加密,方式可以与签名一样。唯一的细微差别是,加密声明总是需要一个 jwe() JwtEncryptionBuilder 过渡,因为该 API 已针对支持签署和声明的内部签名进行了优化。

The claims can be encrypted immediately or after the JSON Web Encryption headers have been set the same way as they can be signed. The only minor difference is that encrypting the claims always requires a jwe() JwtEncryptionBuilder transition given that the API has been optimized to support signing and inner-signing of the claims.

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

Note the alg (key management algorithm) header is set to RSA-OAEP and the enc (content encryption header) is set to A256GCM by default.

可以使用 RSA 和椭圆曲线 (EC) 公钥以及对称密钥对声明进行加密。 ECDH-ESA256KW 分别是 EC 公钥和对称密钥加密算法的默认算法。

RSA and Elliptic Curve (EC) public keys as well as symmetric secret keys can be used to encrypt the claims. ECDH-ES and A256KW are the default algorithms for EC public and symmetric key encryption algorithms respectively.

在创建经过加密令牌时,需执行两种加密操作:

Note two encryption operations are done when creating an encrypted token:

1) 使用 API 提供的密钥加密生成的内容加密密钥,使用密钥加密算法,如 RSA-OAEP`2) 使用生成的内容加密密钥加密声明,使用内容加密算法,如 `A256GCM

1) the generated content encryption key is encrypted by the key supplied with the API using the key encryption algorithm such as RSA-OAEP 2) the claims are encrypted by the generated content encryption key using the content encryption algorithm such as A256GCM.

您可以自定义密钥和内容加密算法,例如:

You can customize the key and content encryption algorithms, for example:

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`属性自定义密钥和内容加密算法:

Alternatively you can use smallrye.jwt.new-token.key-encryption-algorithm and smallrye.jwt.new-token.content-encryption-algorithm properties to customize the key and content encryption algorithms:

smallrye.jwt.new-token.key-encryption-algorithm=RSA-OAEP-256
smallrye.jwt.new-token.content-encryption-algorithm=A256CBC-HS512

并编写一个更简单的 API 序列:

and write a simpler API sequence:

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 端点管理机密密钥,并且只有该端点既是经过加密令牌的生产者,又是消费者。

Note that when the token is directly encrypted by the public RSA or EC key it is not possible to verify which party sent the token. Therefore, the secret keys should be preferred for directly encrypting the tokens, for example, when using JWT as cookies where a secret key is managed by the Quarkus endpoint with only this endpoint being both a producer and a consumer of the encrypted token.

如果您希望使用 RSA 或 EC 公钥加密令牌,则建议先对令牌进行签名(如果签名密钥可用),请参阅下一个 Sign the claims and encrypt the nested JWT token部分。

If you would like to use RSA or EC public keys to encrypt the token then it is recommended to sign the token first if the signing key is available, see the next innersign-encrypt-claims section.

Sign the claims and encrypt the nested JWT token

声明可以先进行签名,然后通过组合签名和加密步骤对嵌套 JWT 令牌进行加密。

The claims can be signed and then the nested JWT token encrypted by combining the sign and encrypt steps.

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):

If smallrye.jwt.sign.key.location or/and smallrye.jwt.encrypt.key.location properties are set then one can secure the existing claims (resources, maps, JsonObjects) with a single call:

// 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(受众)声明。

As mentioned above, iat (issued at), exp (expires at), jti (token identifier), iss (issuer) and aud (audience) claims will be added if needed.

Dealing with the keys

smallrye.jwt.sign.key.location`和 `smallrye.jwt.encrypt.key.location`属性可用于指向签名密钥和加密密钥位置。这些密钥可以位于本地文件系统、类路径上,也可以从远程端点获取,并且可以为 `PEM`或 `JSON Web Key(JWK) 格式。例如:

smallrye.jwt.sign.key.location and smallrye.jwt.encrypt.key.location properties can be used to point to signing and encryption key locations. The keys can be located on the local file system, classpath, or fetched from the remote endpoints and can be in PEM or JSON Web Key (JWK) formats. For example:

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`属性进行替代:

You can also use MicroProfile ConfigSource to fetch the keys from the external services such as HashiCorp Vault or other secret managers and use smallrye.jwt.sign.key and smallrye.jwt.encrypt.key properties instead:

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 编码的私钥或公钥值。

where both private.key.from.vault and public.key.from.vault are the PEM or JWK formatted key values provided by the custom ConfigSource. smallrye.jwt.sign.key and smallrye.jwt.encrypt.key can also contain only the Base64-encoded private or public keys values.

但是,请注意,不建议在配置中直接内联私钥。仅当您需要从远程机密管理器中获取签名密钥值时,再使用 `smallrye.jwt.sign.key`属性。

However, please note, directly inlining the private keys in the configuration is not recommended. Use the smallrye.jwt.sign.key property only if you need to fetch a signing key value from the remote secret manager.

密钥还可以由构建令牌的代码加载并提供给 JWT 构建 API。

The keys can also be loaded by the code which builds the token and supplied to JWT Build API.

如果您需要使用对称机密密钥对令牌进行签名和/或加密,那么请考虑使用 `io.smallrye.jwt.util.KeyUtils`生成所需长度的 SecretKey。

If you need to sign and/or encrypt the token using the symmetric secret key then consider using io.smallrye.jwt.util.KeyUtils to generate a SecretKey of the required length.

例如,使用 HS512`算法 (`512/8) 进行签名需要一个 64 字节的密钥,而使用 A256KW`算法 (`256/8) 加密内容加密密钥需要一个 32 字节的密钥:

For example, one needs to have a 64 byte key to sign using the HS512 algorithm (512/8) and a 32 byte key to encrypt the content encryption key with the A256KW algorithm (256/8):

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`属性引用,例如:

You can also consider using a JSON Web Key (JWK) or JSON Web Key Set (JWK Set) format to store a secret key on a secure file system and refer to it using either smallrye.jwt.sign.key.location or smallrye.jwt.encrypt.key.location properties, for example:

{
 "kty":"oct",
 "kid":"secretKey",
 "k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"
}

or

{
 "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`格式存储。

io.smallrye.jwt.util.KeyUtils can also be used to generate a pair of asymmetric RSA or EC keys. These keys can be stored using a JWK, JWK Set or PEM format.

SmallRye JWT Builder configuration

SmallRye JWT 支持以下属性,可用于自定义对声明进行签名和/或加密的方式:

SmallRye JWT supports the following properties which can be used to customize the way claims are signed and/or encrypted:

Property Name Default Description

smallrye.jwt.sign.key.location

none

Location of a private key which will be used to sign the claims when either a no-argument sign() or innerSign() method is called.

smallrye.jwt.sign.key

none

Key value which will be used to sign the claims when either a no-argument sign() or innerSign() method is called.

smallrye.jwt.sign.key.id

none

Signing key identifier which is checked only when JWK keys are used.

smallrye.jwt.encrypt.key.location

none

Location of a public key which will be used to encrypt the claims or inner JWT when a no-argument encrypt() method is called.

smallrye.jwt.sign.relax-key-validation

false

Relax the validation of the signing keys.

smallrye.jwt.encrypt.key

none

Key value which will be used to encrypt the claims or inner JWT when a no-argument encrypt() method is called.

smallrye.jwt.encrypt.key.id

none

Encryption key identifier which is checked only when JWK keys are used.

smallrye.jwt.encrypt.relax-key-validation

false

Relax the validation of the encryption keys.

smallrye.jwt.new-token.signature-algorithm

RS256

Signature algorithm. This property will be checked if the JWT signature builder has not already set the signature algorithm.

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

RSA-OAEP

Key encryption algorithm. This property will be checked if the JWT encryption builder has not already set the key encryption algorithm.

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

A256GCM

Content encryption algorithm. This property will be checked if the JWT encryption builder has not already set the content encryption algorithm.

smallrye.jwt.new-token.lifespan

300

Token lifespan in seconds which will be used to calculate an exp (expiry) claim value if this claim has not already been set.

smallrye.jwt.new-token.issuer

none

Token issuer which can be used to set an iss (issuer) claim value if this claim has not already been set.

smallrye.jwt.new-token.audience

none

Token audience which can be used to set an aud (audience) claim value if this claim has not already been set.

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

false

Set this property to true for smallrye.jwt.new-token.issuer and smallrye.jwt.new-token.audience values to override the already initialized iss (issuer) and aud (audience) claims.

smallrye.jwt.keystore.type

JKS

This property can be used to customize a keystore type if either smallrye.jwt.sign.key.location or smallrye.jwt.encrypt.key.location or both of these properties point to a KeyStore file. If it is not set then the file name will be checked to determine the keystore type before defaulting to JKS.

smallrye.jwt.keystore.provider

This property can be used to customize a KeyStore provider if smallrye.jwt.sign.key.location or smallrye.jwt.encrypt.key.location point to a KeyStore file.

smallrye.jwt.keystore.password

Keystore password. If smallrye.jwt.sign.key.location or smallrye.jwt.encrypt.key.location point to a KeyStore file then this property has be set.

smallrye.jwt.keystore.encrypt.key.alias

This property has to be set to identify a public encryption key which will be extracted from KeyStore from a matching certificate if smallrye.jwt.encrypt.key.location points to a KeyStore file.

smallrye.jwt.keystore.sign.key.alias

This property has to be set to identify a private signing key if smallrye.jwt.sign.key.location points to a KeyStore file.

smallrye.jwt.keystore.sign.key.password

This property may be set if a private signing key’s password in KeyStore is different to smallrye.jwt.keystore.password when smallrye.jwt.sign.key.location points to a KeyStore file.