MicroProfile JSON Web 令牌(JWT)身份验证
摘要
向红帽构建的 Quarkus 文档提供反馈
要报告错误或改进文档,请登录您的红帽 JIRA 帐户并提交问题。如果您没有红帽 JIRA 帐户,系统会提示您创建一个帐户。
流程
- 单击以下链接 来创建 ticket。
- 在 Summary 中输入有关此问题的简单描述。
- 在描述中提供问题或增强功能的详细描述。请包括有问题的文档 URL。
- 点 Submit 创建问题并将其路由到适当的文档团队。
第 1 章 使用 JWT RBAC
本指南介绍了如何将 SmallRye JWT 集成到 Quarkus 应用中,以根据 MicroProfile JWT 规范实施 JSON Web Token (JWT) 安全性。您将了解如何验证 JWT,将它们代表为 MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken
,以及使用 bearer 令牌授权和基于角色的访问控制来保护 Quarkus HTTP 端点。https://en.wikipedia.org/wiki/Role-based_access_control
Quarkus OpenID Connect (quarkus-oidc
)扩展也支持 bearer 令牌授权,并使用 smallrye-jwt
来代表 bearer 令牌作为 JsonWebToken
。详情请查看 OIDC Bearer Token Authentication 指南。
如果您的 Quarkus 应用程序需要使用 OIDC 授权代码流验证用户,则必须使用 OpenID Connect 扩展。如需更多信息,请参阅 用于保护 Web 应用程序的 OIDC 代码流机制。
1.1. 先决条件
要完成本指南,您需要:
- 大约 15 分钟
- IDE
-
正确配置了
JAVA_HOME
的 JDK 17+ - Apache Maven 3.8.6 或更高版本
- 如果要使用 Quarkus CLI,可选
- 如果要构建原生可执行文件(或者使用原生容器构建,则可选的 Mandrel 或 GraalVM) https://quarkus.io/version/3.20/guides/building-native-image#configuring-graalvm
1.2. Quickstart
1.2.1. 解决方案
我们建议您按照后续部分中的说明来逐步创建应用程序步骤。如果您愿意,可以进入已完成的示例。
要访问示例,请克隆 Git 存储库或下载存档:
-
克隆存储库:
git clone https://github.com/quarkusio/quarkus-quickstarts.git -b 3.20
。 - 下载 存档。
已完成的解决方案位于 security-jwt-quickstart
目录中。
1.2.2. 创建 Maven 项目
首先,使用以下命令创建新项目:
使用 Quarkus CLI:
quarkus create app org.acme:security-jwt-quickstart \ --extension='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ --no-code cd security-jwt-quickstart
Copy to clipboardCopiedquarkus create app org.acme:security-jwt-quickstart \ --extension='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ --no-code cd security-jwt-quickstart
要创建 Gradle 项目,请添加--
gradle or
--gradle-kotlin-dsl
选项。有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。
使用 Maven:
mvn com.redhat.quarkus.platform:quarkus-maven-plugin:3.20.1:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=security-jwt-quickstart \ -Dextensions='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ -DnoCode cd security-jwt-quickstart
Copy to clipboardCopiedmvn com.redhat.quarkus.platform:quarkus-maven-plugin:3.20.1:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=security-jwt-quickstart \ -Dextensions='rest-jackson,smallrye-jwt,smallrye-jwt-build' \ -DnoCode cd security-jwt-quickstart
要创建 Gradle 项目,请添加
-DbuildTool=gradle
or-DbuildTool=gradle-kotlin-dsl
选项。
对于 Windows 用户:
-
如果使用 cmd,(不要使用反向斜杠
\
并将所有内容放在同一行中) -
如果使用 Powershell,则双引号中的 wrap
-D
参数,如"-DprojectArtifactId=security-jwt-quickstart"
此命令生成 Maven 项目并导入 smallrye-jwt
扩展,其包含 MicroProfile JWT RBAC 支持。
如果您已经配置了 Quarkus 项目,您可以通过在项目基本目录中运行以下命令来将 smallrye-jwt
扩展添加到项目中:
使用 Quarkus CLI:
quarkus extension add smallrye-jwt,smallrye-jwt-build
Copy to clipboardCopiedquarkus extension add smallrye-jwt,smallrye-jwt-build
使用 Maven:
./mvnw quarkus:add-extension -Dextensions='smallrye-jwt,smallrye-jwt-build'
Copy to clipboardCopied./mvnw quarkus:add-extension -Dextensions='smallrye-jwt,smallrye-jwt-build'
使用 Gradle:
./gradlew addExtension --extensions='smallrye-jwt,smallrye-jwt-build'
Copy to clipboardCopied./gradlew addExtension --extensions='smallrye-jwt,smallrye-jwt-build'
这个命令在构建文件中添加以下依赖项:
使用 Maven:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
Copy to clipboardCopied<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
使用 Gradle:
implementation("io.quarkus:quarkus-smallrye-jwt") implementation("io.quarkus:quarkus-smallrye-jwt-build")
Copy to clipboardCopiedimplementation("io.quarkus:quarkus-smallrye-jwt") implementation("io.quarkus:quarkus-smallrye-jwt-build")
1.2.3. 检查 Jakarta REST 资源
在 src/main/java/org/acme/security/jwt/TokenSecuredResource.java
中创建一个 REST 端点,其内容如下:
REST 端点 V1
package org.acme.security.jwt; import jakarta.annotation.security.PermitAll; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") public class TokenSecuredResource { @Inject JsonWebToken jwt; 1 @GET @Path("permit-all") @PermitAll 2 @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { return getResponseString(ctx); 3 } private String getResponseString(SecurityContext ctx) { String name; if (ctx.getUserPrincipal() == null) { 4 name = "anonymous"; } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { 5 throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); } else { name = ctx.getUserPrincipal().getName(); 6 } return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); 7 } private boolean hasJwt() { return jwt.getClaimNames() != null; } }
package org.acme.security.jwt;
import jakarta.annotation.security.PermitAll;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
public class TokenSecuredResource {
@Inject
JsonWebToken jwt; 1
@GET
@Path("permit-all")
@PermitAll 2
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx); 3
}
private String getResponseString(SecurityContext ctx) {
String name;
if (ctx.getUserPrincipal() == null) { 4
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { 5
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName(); 6
}
return String.format("hello %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); 7
}
private boolean hasJwt() {
return jwt.getClaimNames() != null;
}
}
Copy to clipboardCopied- 1
JsonWebToken
接口被注入,提供对与当前经过身份验证的用户关联的声明的访问。这个接口扩展了java.security.Principal
。- 2
@PermitAll
是标准的 Jakarta 安全注释。表示给定端点可以被所有调用者访问,无论是否通过身份验证。- 3
- Jakarta REST
SecurityContext
被注入,以检查请求的安全状态。getResponseString ()
函数生成响应。 - 4
- 通过检查请求用户/调用者
Principal
是否针对 null 检查调用是否不安全。 - 5
- 确保
Principal
和JsonWebToken
中的名称与 JsonWebToken 匹配,因为JsonWebToken
代表当前的主体
。 - 6
- 检索
Principal
的名称。 - 7
- 构建包含调用者名称的
isSecure ()
和getAuthenticationScheme ()
状态的响应,以及是否注入了非nullJsonWebToken
。
1.2.4. 在 dev 模式下运行应用程序
现在,您可以使用以下命令以 dev 模式运行应用程序:
使用 Quarkus CLI:
quarkus dev
Copy to clipboardCopiedquarkus dev
使用 Maven:
./mvnw quarkus:dev
Copy to clipboardCopied./mvnw quarkus:dev
使用 Gradle:
./gradlew --console=plain quarkusDev
Copy to clipboardCopied./gradlew --console=plain quarkusDev
然后,您应该看到类似以下示例的输出:
quarkus:dev
output
[INFO] Scanning for projects... [INFO] [INFO] ----------------------< org.acme:security-jwt-quickstart >----------------------- [INFO] Building security-jwt-quickstart 1.0.0-SNAPSHOT [INFO] --------------------------------[ jar ]--------------------------------- ... Listening for transport dt_socket at address: 5005 2020-07-15 16:09:50,883 INFO [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080 2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated. 2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, rest, rest-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------------< org.acme:security-jwt-quickstart >-----------------------
[INFO] Building security-jwt-quickstart 1.0.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
...
Listening for transport dt_socket at address: 5005
2020-07-15 16:09:50,883 INFO [io.quarkus] (Quarkus Main Thread) security-jwt-quickstart 1.0.0-SNAPSHOT on JVM (powered by Quarkus 999-SNAPSHOT) started in 1.073s. Listening on: http://0.0.0.0:8080
2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Profile dev activated. Live Coding activated.
2020-07-15 16:09:50,885 INFO [io.quarkus] (Quarkus Main Thread) Installed features: [cdi, mutiny, rest, rest-jackson, security, smallrye-context-propagation, smallrye-jwt, vertx, vertx-web]
Copy to clipboardCopied现在,REST 端点正在运行,您可以使用命令行工具(如 curl)访问它:
/secured/permit-all
的 curl
命令
curl http://127.0.0.1:8080/secured/permit-all; echo
$ curl http://127.0.0.1:8080/secured/permit-all; echo
Copy to clipboardCopied这个命令返回以下响应:
hello anonymous, isHttps: false, authScheme: null, hasJWT: false
hello anonymous, isHttps: false, authScheme: null, hasJWT: false
Copy to clipboardCopied您没有在我们的请求中提供任何 JWT,因此您不会预期端点查看任何安全状态,并且响应与其一致:
-
用户名是
匿名的。 -
isHttps
为false
,因为不使用https
。 -
authScheme
是null
。 -
的JWT
为false
。
使用 Ctrl-C 停止 Quarkus 服务器。
现在,我们来保证.查看新端点方法 helloRolesAllowed
,如下所示:
REST 端点 V2
package org.acme.security.jwt; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") public class TokenSecuredResource { @Inject JsonWebToken jwt; 1 @GET @Path("permit-all") @PermitAll @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { return getResponseString(ctx); } @GET @Path("roles-allowed") 2 @RolesAllowed({ "User", "Admin" }) 3 @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowed(@Context SecurityContext ctx) { return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); 4 } private String getResponseString(SecurityContext ctx) { String name; if (ctx.getUserPrincipal() == null) { name = "anonymous"; } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); } else { name = ctx.getUserPrincipal().getName(); } return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); } private boolean hasJwt() { return jwt.getClaimNames() != null; } }
package org.acme.security.jwt;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
public class TokenSecuredResource {
@Inject
JsonWebToken jwt; 1
@GET
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx);
}
@GET
@Path("roles-allowed") 2
@RolesAllowed({ "User", "Admin" }) 3
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowed(@Context SecurityContext ctx) {
return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); 4
}
private String getResponseString(SecurityContext ctx) {
String name;
if (ctx.getUserPrincipal() == null) {
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName();
}
return String.format("hello %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
}
private boolean hasJwt() {
return jwt.getClaimNames() != null;
}
}
Copy to clipboardCopied
在添加了 TokenSecuredResource
后,重新运行 ./mvnw quarkus:dev
命令,然后尝试 curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
以尝试访问新端点。
您的输出应如下所示:
/secured/roles-allowed
的 curl
命令
curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
Copy to clipboardCopied这个命令返回以下响应:
* Trying 127.0.0.1... * TCP_NODELAY set * Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0) > GET /secured/roles-allowed HTTP/1.1 > Host: 127.0.0.1:8080 > User-Agent: curl/7.54.0 > Accept: */* > < HTTP/1.1 401 Unauthorized < Connection: keep-alive < Content-Type: text/html;charset=UTF-8 < Content-Length: 14 < Date: Sun, 03 Mar 2019 16:32:34 GMT < * Connection #0 to host 127.0.0.1 left intact
* Trying 127.0.0.1...
* TCP_NODELAY set
* Connected to 127.0.0.1 (127.0.0.1) port 8080 (#0)
> GET /secured/roles-allowed HTTP/1.1
> Host: 127.0.0.1:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 401 Unauthorized
< Connection: keep-alive
< Content-Type: text/html;charset=UTF-8
< Content-Length: 14
< Date: Sun, 03 Mar 2019 16:32:34 GMT
<
* Connection #0 to host 127.0.0.1 left intact
Copy to clipboardCopied卓越的.您没有在请求中提供 JWT,因此正确拒绝对端点的访问。相反,您会收到 HTTP 401 Unauthorized 错误。
若要访问端点,您必须获取并在请求中包含有效的 JWT。这涉及两个步骤:
- 使用必要信息配置 SmallRye JWT 扩展以验证 JWT。
- 使用适当的声明生成 JWT 以匹配配置。
1.2.5. 配置 SmallRye JWT 扩展安全信息
使用以下内容创建 security-jwt-quickstart/src/main/resources/application.properties
:
TokenSecuredResource
的应用程序属性
mp.jwt.verify.publickey.location=publicKey.pem 1 mp.jwt.verify.issuer=https://example.com/issuer 2 quarkus.native.resources.includes=publicKey.pem 3
mp.jwt.verify.publickey.location=publicKey.pem 1
mp.jwt.verify.issuer=https://example.com/issuer 2
quarkus.native.resources.includes=publicKey.pem 3
Copy to clipboardCopied- 1
- 指定 classpath 上公钥文件
publicKey.pem
的位置。请参阅为 添加此密钥 添加公钥。 - 2
- 将预期的签发者定义为
https://example.com/issuer
。 - 3
- 确保
publicKey.pem
文件作为原生可执行文件中的资源包含。
1.2.6. 添加公钥
JWT 规范定义 可以使用的 JWT 的各种安全性级别。MicroProfile JWT RBAC 规范需要使用 RSA-256 签名算法签名的 JWT。这反过来需要一个 RSA 公钥对。在 REST 端点服务器端,您需要配置 RSA 公钥的位置,以验证与请求一起发送的 JWT。mp.jwt.verify.publickey.location=publicKey.pem
设置预期之前配置的公钥为 publicKey.pem
。要达到此目的,请将以下内容复制到 security-jwt-quickstart/src/main/resources/publicKey.pem
文件中。
RSA 公钥 PEM 内容
-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9 AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x nQIDAQAB -----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----
Copy to clipboardCopied1.2.7. 生成 JWT
通常,从 Keycloak 等身份管理器获取 JWT。但是,对于此快速入门,您可以使用 smallrye-jwt
提供的 JWT 生成 API 自行生成。如需更多信息,请参阅使用 SmallRye JWT 生成 JWT 令牌。
从以下列表中获取代码,并将它放入 security-jwt-quickstart/src/test/java/org/acme/security/jwt/GenerateToken.java
中:
GenerateToken
主驱动程序类
package org.acme.security.jwt; import java.util.Arrays; import java.util.HashSet; import org.eclipse.microprofile.jwt.Claims; import io.smallrye.jwt.build.Jwt; /** * A utility class to generate and print a JWT token string to stdout. */ public class GenerateToken { /** * Generates and prints a JWT token. */ public static void main(String[] args) { String token = Jwt.issuer("https://example.com/issuer") 1 .upn("jdoe@quarkus.io") 2 .groups(new HashSet<>(Arrays.asList("User", "Admin"))) 3 .claim(Claims.birthdate.name(), "2001-07-13") 4 .sign(); System.out.println(token); System.exit(0); } }
package org.acme.security.jwt;
import java.util.Arrays;
import java.util.HashSet;
import org.eclipse.microprofile.jwt.Claims;
import io.smallrye.jwt.build.Jwt;
/**
* A utility class to generate and print a JWT token string to stdout.
*/
public class GenerateToken {
/**
* Generates and prints a JWT token.
*/
public static void main(String[] args) {
String token = Jwt.issuer("https://example.com/issuer") 1
.upn("jdoe@quarkus.io") 2
.groups(new HashSet<>(Arrays.asList("User", "Admin"))) 3
.claim(Claims.birthdate.name(), "2001-07-13") 4
.sign();
System.out.println(token);
System.exit(0);
}
}
Copy to clipboardCopied- 1
- 在 JWT 中设置
iss
(issuer)声明。这个值必须与服务器端mp.jwt.verify.issuer
配置匹配,以便令牌被视为有效。 - 2
- 指定
upn
(User Principal Name)声明,MicroProfile JWT RBAC 规范将它定义为识别容器安全 API 中Principal
的首选声明。 - 3
- 定义
groups
声明,它提供组成员资格和分配给 JWT bearer 的顶级角色。 - 4
- 添加
birthdate
声明。由于这被视为敏感信息,因此请考虑加密声明,如 使用 SmallRye JWT 生成 JWT 令牌 中所述。
请注意,为此代码正常工作,您需要与您在 TokenSecuredResource
应用程序中的公钥对应的 RSA 私钥的内容。取以下 PEM 内容并将其放入 security-jwt-quickstart/src/test/resources/privateKey.pem
中:
RSA 私钥 PEM 内容
-----BEGIN PRIVATE KEY----- MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6 oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv 6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6 96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3 zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS 34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG 5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2 tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09 nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8 f3cg+fr8aou7pr9SHhJlZCU= -----END PRIVATE KEY-----
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCWK8UjyoHgPTLa
PLQJ8SoXLLjpHSjtLxMqmzHnFscqhTVVaDpCRCb6e3Ii/WniQTWw8RA7vf4djz4H
OzvlfBFNgvUGZHXDwnmGaNVaNzpHYFMEYBhE8VGGiveSkzqeLZI+Y02G6sQAfDtN
qqzM/l5QX8X34oQFaTBW1r49nftvCpITiwJvWyhkWtXP9RP8sXi1im5Vi3dhupOh
nelk5n0BfajUYIbfHA6ORzjHRbt7NtBl0L2J+0/FUdHyKs6KMlFGNw8O0Dq88qnM
uXoLJiewhg9332W3DFMeOveel+//cvDnRsCRtPgd4sXFPHh+UShkso7+DRsChXa6
oGGQD3GdAgMBAAECggEAAjfTSZwMHwvIXIDZB+yP+pemg4ryt84iMlbofclQV8hv
6TsI4UGwcbKxFOM5VSYxbNOisb80qasb929gixsyBjsQ8284bhPJR7r0q8h1C+jY
URA6S4pk8d/LmFakXwG9Tz6YPo3pJziuh48lzkFTk0xW2Dp4SLwtAptZY/+ZXyJ6
96QXDrZKSSM99Jh9s7a0ST66WoxSS0UC51ak+Keb0KJ1jz4bIJ2C3r4rYlSu4hHB
Y73GfkWORtQuyUDa9yDOem0/z0nr6pp+pBSXPLHADsqvZiIhxD/O0Xk5I6/zVHB3
zuoQqLERk0WvA8FXz2o8AYwcQRY2g30eX9kU4uDQAQKBgQDmf7KGImUGitsEPepF
KH5yLWYWqghHx6wfV+fdbBxoqn9WlwcQ7JbynIiVx8MX8/1lLCCe8v41ypu/eLtP
iY1ev2IKdrUStvYRSsFigRkuPHUo1ajsGHQd+ucTDf58mn7kRLW1JGMeGxo/t32B
m96Af6AiPWPEJuVfgGV0iwg+HQKBgQCmyPzL9M2rhYZn1AozRUguvlpmJHU2DpqS
34Q+7x2Ghf7MgBUhqE0t3FAOxEC7IYBwHmeYOvFR8ZkVRKNF4gbnF9RtLdz0DMEG
5qsMnvJUSQbNB1yVjUCnDAtElqiFRlQ/k0LgYkjKDY7LfciZl9uJRl0OSYeX/qG2
tRW09tOpgQKBgBSGkpM3RN/MRayfBtmZvYjVWh3yjkI2GbHA1jj1g6IebLB9SnfL
WbXJErCj1U+wvoPf5hfBc7m+jRgD3Eo86YXibQyZfY5pFIh9q7Ll5CQl5hj4zc4Y
b16sFR+xQ1Q9Pcd+BuBWmSz5JOE/qcF869dthgkGhnfVLt/OQzqZluZRAoGAXQ09
nT0TkmKIvlza5Af/YbTqEpq8mlBDhTYXPlWCD4+qvMWpBII1rSSBtftgcgca9XLB
MXmRMbqtQeRtg4u7dishZVh1MeP7vbHsNLppUQT9Ol6lFPsd2xUpJDc6BkFat62d
Xjr3iWNPC9E9nhPPdCNBv7reX7q81obpeXFMXgECgYEAmk2Qlus3OV0tfoNRqNpe
Mb0teduf2+h3xaI1XDIzPVtZF35ELY/RkAHlmWRT4PCdR0zXDidE67L6XdJyecSt
FdOUH8z5qUraVVebRFvJqf/oGsXc4+ex1ZKUTbY0wqY1y9E39yvB3MaTmZFuuqk8
f3cg+fr8aou7pr9SHhJlZCU=
-----END PRIVATE KEY-----
Copy to clipboardCopied
之后,您要配置 smallrye.jwt.sign.key.location
属性来指定私钥的位置。
也可以使用 OpenSSL 命令行工具生成公钥和私钥对。
用于生成密钥的 OpenSSL 命令
openssl genrsa -out rsaPrivateKey.pem 2048 openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem
openssl genrsa -out rsaPrivateKey.pem 2048
openssl rsa -pubout -in rsaPrivateKey.pem -out publicKey.pem
Copy to clipboardCopied需要额外的步骤来生成私钥并将其转换为 PKCS"8 格式,通常用于安全密钥存储和传输。
用于执行转换的 OpenSSL 命令
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem
openssl pkcs8 -topk8 -nocrypt -inform pem -in rsaPrivateKey.pem -outform pem -out privateKey.pem
Copy to clipboardCopied您可以使用生成的密钥对而不是这个快速入门中使用的密钥对。
在为 TokenSecuredResource
端点生成 JSON Web Token (JWT)之前,请确保 应用正在运行。
接下来,使用以下命令生成 JWT:
JWT 生成输出示例
mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem
$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem
Copy to clipboardCopied
JWT 字符串是由三个部分组成的 Base64 URL 编码字符串,用 .
字符分隔:
- 标头,其中包含令牌的元数据,如签名算法。
- 有效负载也称为 "claims",其中包括令牌的声明或数据。
- 签名,验证令牌的完整性。
1.2.8. 最后,对 /secured/roles-allowed
进行安全访问
现在,让我们使用它来向 /secured/roles-allowed
端点发出安全请求。确保 Quarkus 服务器仍然以 dev 模式运行,然后运行以下命令,确保使用上一步中生成的 JWT 版本:
使用 JWT 进行 /secured/roles-allowed
的 curl
命令
curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
Copy to clipboardCopied确保将生成的令牌用作 HTTP Authorization Bearer 方案值。
这个命令返回以下响应:
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
Copy to clipboardCopied成功!现在您有以下内容:
-
非匿名调用者名称:
jdoe@quarkus.io
-
身份验证方案:
Bearer
-
非null
JsonWebToken
-
birthdate
声明值
1.2.9. 使用 JsonWebToken
和声明注入
现在,您可以生成 JWT 以访问我们的安全 REST 端点,让我们看到您可以使用 JsonWebToken
接口和 JWT 声明进行更多的操作。org.eclipse.microprofile.jwt.JsonWebToken
接口扩展了 java.security.Principal
接口,并且是由之前使用的 jakarta.ws.ws.rs.core.SecurityContext SerialgetUserPrincipal ()
调用返回的对象类型。这意味着,不使用 CDI 但可以访问 REST 容器 SecurityContext
的代码可以存放调用者 JsonWebToken
接口。
JsonWebToken
接口定义了在底层 JWT 中访问声明的方法。它为 MicroProfile JWT RBAC 规范和 JWT 中可能存在的任意声明提供常见声明的访问权限。
也可以注入所有 JWT 声明。我们使用另一个端点 /secured/roles-allowed-admin
扩展 TokenSecuredResource
,该端点使用注入的 birthdate
声明(而不是从 JsonWebToken
获取):
package org.acme.security.jwt; import jakarta.annotation.security.PermitAll; import jakarta.annotation.security.RolesAllowed; import jakarta.enterprise.context.RequestScoped; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.InternalServerErrorException; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.Context; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.SecurityContext; import org.eclipse.microprofile.jwt.Claim; import org.eclipse.microprofile.jwt.Claims; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/secured") @RequestScoped 1 public class TokenSecuredResource { @Inject JsonWebToken jwt; 2 @Inject @Claim(standard = Claims.birthdate) String birthdate; 3 @GET @Path("permit-all") @PermitAll @Produces(MediaType.TEXT_PLAIN) public String hello(@Context SecurityContext ctx) { return getResponseString(ctx); } @GET @Path("roles-allowed") @RolesAllowed({ "User", "Admin" }) @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowed(@Context SecurityContext ctx) { return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString(); } @GET @Path("roles-allowed-admin") @RolesAllowed("Admin") @Produces(MediaType.TEXT_PLAIN) public String helloRolesAllowedAdmin(@Context SecurityContext ctx) { return getResponseString(ctx) + ", birthdate: " + birthdate; 4 } private String getResponseString(SecurityContext ctx) { String name; if (ctx.getUserPrincipal() == null) { name = "anonymous"; } else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) { throw new InternalServerErrorException("Principal and JsonWebToken names do not match"); } else { name = ctx.getUserPrincipal().getName(); } return String.format("hello %s," + " isHttps: %s," + " authScheme: %s," + " hasJWT: %s", name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt()); } private boolean hasJwt() { return jwt.getClaimNames() != null; } }
package org.acme.security.jwt;
import jakarta.annotation.security.PermitAll;
import jakarta.annotation.security.RolesAllowed;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.InternalServerErrorException;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.Context;
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.SecurityContext;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/secured")
@RequestScoped 1
public class TokenSecuredResource {
@Inject
JsonWebToken jwt; 2
@Inject
@Claim(standard = Claims.birthdate)
String birthdate; 3
@GET
@Path("permit-all")
@PermitAll
@Produces(MediaType.TEXT_PLAIN)
public String hello(@Context SecurityContext ctx) {
return getResponseString(ctx);
}
@GET
@Path("roles-allowed")
@RolesAllowed({ "User", "Admin" })
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowed(@Context SecurityContext ctx) {
return getResponseString(ctx) + ", birthdate: " + jwt.getClaim("birthdate").toString();
}
@GET
@Path("roles-allowed-admin")
@RolesAllowed("Admin")
@Produces(MediaType.TEXT_PLAIN)
public String helloRolesAllowedAdmin(@Context SecurityContext ctx) {
return getResponseString(ctx) + ", birthdate: " + birthdate; 4
}
private String getResponseString(SecurityContext ctx) {
String name;
if (ctx.getUserPrincipal() == null) {
name = "anonymous";
} else if (!ctx.getUserPrincipal().getName().equals(jwt.getName())) {
throw new InternalServerErrorException("Principal and JsonWebToken names do not match");
} else {
name = ctx.getUserPrincipal().getName();
}
return String.format("hello %s,"
+ " isHttps: %s,"
+ " authScheme: %s,"
+ " hasJWT: %s",
name, ctx.isSecure(), ctx.getAuthenticationScheme(), hasJwt());
}
private boolean hasJwt() {
return jwt.getClaimNames() != null;
}
}
Copy to clipboardCopied现在再次生成令牌并运行:
curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
Copy to clipboardCopied确保将生成的令牌用作 HTTP Authorization Bearer 方案值。
这个命令返回以下响应:
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
Copy to clipboardCopied1.2.10. 在 JVM 模式下运行应用程序
您可以将应用程序作为标准 Java 应用程序运行。
编译应用程序:
使用 Quarkus CLI:
quarkus build
Copy to clipboardCopiedquarkus build
使用 Maven:
./mvnw install
Copy to clipboardCopied./mvnw install
使用 Gradle:
./gradlew build
Copy to clipboardCopied./gradlew build
运行应用程序:
java -jar target/quarkus-app/quarkus-run.jar
Copy to clipboardCopiedjava -jar target/quarkus-app/quarkus-run.jar
1.2.11. 以原生模式运行应用程序
您可以在不进行任何修改的情况下,将相同的演示编译为原生模式。这意味着您不再需要在生产环境中安装 JVM。运行时技术包含在生成的二进制中,并经过优化,以便以最少的资源运行。
编译时间需要一些时间,因此默认禁用此步骤。
通过启用
原生
配置集来再次构建应用程序:使用 Quarkus CLI:
quarkus build --native
Copy to clipboardCopiedquarkus build --native
使用 Maven:
./mvnw install -Dnative
Copy to clipboardCopied./mvnw install -Dnative
使用 Gradle:
./gradlew build -Dquarkus.native.enabled=true
Copy to clipboardCopied./gradlew build -Dquarkus.native.enabled=true
直接运行以下二进制文件:
./target/security-jwt-quickstart-1.0.0-SNAPSHOT-runner
Copy to clipboardCopied./target/security-jwt-quickstart-1.0.0-SNAPSHOT-runner
1.2.12. 探索解决方案
security-jwt-quickstart
目录 存储库包含此快速入门指南中涵盖的所有版本,以及用于使用注入的 JsonWebToken
令牌及其声明通过 CDI API 演示子资源的其他端点。
我们建议您探索 security-jwt-quickstart
目录,并查看快速入门解决方案,以了解更多有关 SmallRye JWT 扩展的功能。
1.3. 参考指南
1.3.1. 支持的注入范围
当 org.eclipse.microprofile.jwt.JsonWebToken
被注入时,@ApplicationScoped
、@Singleton
和
outer bean 注入范围都是支持。@RequestScoped
但是,当将单个令牌声明注入为简单类型(如 String
)时,必须使用 @RequestScoped
,例如:
package org.acme.security.jwt; import jakarta.inject.Inject; import org.eclipse.microprofile.jwt.Claim; import org.eclipse.microprofile.jwt.Claims; @Path("/secured") @RequestScoped public class TokenSecuredResource { @Inject @Claim(standard = Claims.birthdate) String birthdate; }
package org.acme.security.jwt;
import jakarta.inject.Inject;
import org.eclipse.microprofile.jwt.Claim;
import org.eclipse.microprofile.jwt.Claims;
@Path("/secured")
@RequestScoped
public class TokenSecuredResource {
@Inject
@Claim(standard = Claims.birthdate)
String birthdate;
}
Copy to clipboardCopied
请注意,您也可以使用注入的 JsonWebToken
来访问单个声明,但本例中不需要设置 @RequestScoped
。
如需了解更多详细信息,请参阅 MP JWT CDI Injection 要求。
1.3.2. 支持的公钥格式
可以使用以下格式格式化公钥,按优先级顺序指定:
- 公钥加密标准 #8 (PKCS""8) PEM
- JSON Web 密钥(JWK)
- JSON Web 密钥集(JWKS)
- JSON Web 密钥(JWK) Base64 URL 编码
- JSON Web 密钥集(JWKS) Base64 URL 编码
1.3.3. 处理验证密钥
如果您需要使用非对称 RSA 或 Elliptic Curve (EC)密钥验证令牌签名,请使用 mp.jwt.verify.publickey.location
属性来引用本地或远程验证密钥。
使用 mp.jwt.verify.publickey.algorithm
自定义验证算法(默认为 RS256
),例如,在使用 EC 密钥时将其设置为 ES256
。
如果您需要使用对称 secret 密钥验证令牌签名,则必须使用 JSON Web
密钥集(JWK)或 JSON Web 密钥集
(JWK Set)格式来代表此 secret 密钥,例如:
{ "keys": [ { "kty":"oct", "kid":"secretKey", "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" } ] }
{
"keys": [
{
"kty":"oct",
"kid":"secretKey",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
]
}
Copy to clipboardCopied
此机密密钥 JWK 还必须通过 smallrye.jwt.verify.key.location
来引用。smallrye.jwt.verify.algorithm
应设置为 HS256
/HS384
/HS512
。
1.3.4. 使用 JWTParser
解析并验证 JsonWebToken
如果 JWT 令牌不能注入,例如,如果在服务请求有效负载中嵌入它,或者服务端点将其从带外获取,则用户可以使用 JWTParser
:
import org.eclipse.microprofile.jwt.JsonWebToken; import io.smallrye.jwt.auth.principal.JWTParser; ... @Inject JWTParser parser; String token = getTokenFromOidcServer(); // Parse and verify the token JsonWebToken jwt = parser.parse(token);
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
...
@Inject JWTParser parser;
String token = getTokenFromOidcServer();
// Parse and verify the token
JsonWebToken jwt = parser.parse(token);
Copy to clipboardCopied
您还可以使用它来自定义令牌被验证或解密的方式。例如,一个可以提供本地 SecretKey
:
package org.acme.security.jwt; import io.smallrye.jwt.auth.principal.ParseException; import jakarta.inject.Inject; import jakarta.ws.rs.CookieParam; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.NewCookie; import jakarta.ws.rs.core.Response; import org.eclipse.microprofile.jwt.JsonWebToken; import io.smallrye.jwt.auth.principal.JWTParser; import io.smallrye.jwt.build.Jwt; @Path("/secured") public class SecuredResource { private static final String SECRET = "AyM1SysPpbyDfgZld3umj1qzKObwVMko"; @Inject JWTParser parser; @GET @Produces("text/plain") public Response getUserName(@CookieParam("jwt") String jwtCookie) throws ParseException { if (jwtCookie == null) { // Create a JWT token signed by using the 'HS256' algorithm String newJwtCookie = Jwt.upn("Alice").signWithSecret(SECRET); // or create a JWT token encrypted by using the 'A256KW' algorithm // Jwt.upn("alice").encryptWithSecret(secret); return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build(); } else { // All mp.jwt and smallrye.jwt properties are still effective; only the verification key is customized. JsonWebToken jwt = parser.verify(jwtCookie, SECRET); // or jwt = parser.decrypt(jwtCookie, secret); return Response.ok(jwt.getName()).build(); } } }
package org.acme.security.jwt;
import io.smallrye.jwt.auth.principal.ParseException;
import jakarta.inject.Inject;
import jakarta.ws.rs.CookieParam;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.NewCookie;
import jakarta.ws.rs.core.Response;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.smallrye.jwt.auth.principal.JWTParser;
import io.smallrye.jwt.build.Jwt;
@Path("/secured")
public class SecuredResource {
private static final String SECRET = "AyM1SysPpbyDfgZld3umj1qzKObwVMko";
@Inject
JWTParser parser;
@GET
@Produces("text/plain")
public Response getUserName(@CookieParam("jwt") String jwtCookie) throws ParseException {
if (jwtCookie == null) {
// Create a JWT token signed by using the 'HS256' algorithm
String newJwtCookie = Jwt.upn("Alice").signWithSecret(SECRET);
// or create a JWT token encrypted by using the 'A256KW' algorithm
// Jwt.upn("alice").encryptWithSecret(secret);
return Response.ok("Alice").cookie(new NewCookie("jwt", newJwtCookie)).build();
} else {
// All mp.jwt and smallrye.jwt properties are still effective; only the verification key is customized.
JsonWebToken jwt = parser.verify(jwtCookie, SECRET);
// or jwt = parser.decrypt(jwtCookie, secret);
return Response.ok(jwt.getName()).build();
}
}
}
Copy to clipboardCopied
另请参阅 How to Add SmallRye JWT 部分有关如何使用 JWTParser
部分,而无需 quarkus-smallrye-jwt
提供的 HTTP
支持。
1.3.5. 令牌解密
如果您的应用需要接受带有加密声明或加密声明的令牌,只需设置 smallrye.jwt.decrypt.key.location
属性以指向解密密钥。
如果这是唯一设定的 key 属性,则传入的令牌应该仅包含加密的声明。如果同时设置了 mp.jwt.verify.publickey
或 mp.jwt.verify.publickey.location
验证属性,则传入的令牌应该包含加密的内部令牌。
请参阅 使用 SmallRye JWT 生成 JWT 令牌,了解如何生成加密或内部签名,然后快速生成加密的令牌。
1.3.6. 自定义工厂
io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory
是用于解析和验证 JWT 令牌的默认实施,将它们转换为 JsonWebToken
主体。此工厂依赖于 MP JWT
和 smallrye-jwt
属性,如 Configuration
部分所述,以验证和自定义 JWT 令牌。
如果您需要实现自定义工厂(如跳过已由防火墙验证的令牌),您可以使用以下方法之一完成此操作:
-
通过创建
META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory
资源来使用ServiceLoader
机制。 -
提供
替代
CDI bean 实现,如下例所示:
import java.nio.charset.StandardCharsets; import java.util.Base64; import jakarta.annotation.Priority; import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Alternative; import org.jose4j.jwt.JwtClaims; import org.jose4j.jwt.consumer.InvalidJwtException; import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal; import io.smallrye.jwt.auth.principal.JWTAuthContextInfo; import io.smallrye.jwt.auth.principal.JWTCallerPrincipal; import io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory; import io.smallrye.jwt.auth.principal.ParseException; @ApplicationScoped @Alternative @Priority(1) public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory { @Override public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException { try { // Token has already been verified; parse the token claims only String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8); return new DefaultJWTCallerPrincipal(JwtClaims.parse(json)); } catch (InvalidJwtException ex) { throw new ParseException(ex.getMessage()); } } }
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Alternative;
import org.jose4j.jwt.JwtClaims;
import org.jose4j.jwt.consumer.InvalidJwtException;
import io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTAuthContextInfo;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipal;
import io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory;
import io.smallrye.jwt.auth.principal.ParseException;
@ApplicationScoped
@Alternative
@Priority(1)
public class TestJWTCallerPrincipalFactory extends JWTCallerPrincipalFactory {
@Override
public JWTCallerPrincipal parse(String token, JWTAuthContextInfo authContextInfo) throws ParseException {
try {
// Token has already been verified; parse the token claims only
String json = new String(Base64.getUrlDecoder().decode(token.split("\\.")[1]), StandardCharsets.UTF_8);
return new DefaultJWTCallerPrincipal(JwtClaims.parse(json));
} catch (InvalidJwtException ex) {
throw new ParseException(ex.getMessage());
}
}
}
Copy to clipboardCopied1.3.7. 阻塞调用
quarkus-smallrye-jwt
扩展使用 SmallRye JWT 库,该库目前不是被动的。
从 quarkus-smallrye-jwt
的视角中,它作为 reactive Quarkus 安全架构的一部分运行,是输入 SmallRye JWT 验证或解密代码之一的 IO 线程可能会在以下情况之一中阻止:
-
默认密钥解析器会刷新包含密钥的
JsonWebKey
设置,该密钥涉及对 OIDC 端点的远程调用。 -
自定义密钥解析器,如
AWS Application Load Balancer
(ALB
)密钥解析器,使用当前令牌的密钥标识符标头值针对 AWS ALB 密钥端点解析密钥。
在这种情况下,如果连接速度较慢,对键端点需要超过 3 秒的时间响应 - 当前事件循环线程可能会被阻止。
要防止它阻止,请设置 quarkus.smallrye-jwt.blocking-authentication=true
。
1.3.8. 令牌传播
请参阅有关 Bearer 访问令牌传播到下游服务的 Token Propagation 部分。
1.3.9. 测试
1.3.9.1. Wiremock
如果您将 mp.jwt.verify.publickey.location
配置为指向 HTTPS 或基于 HTTP 的 JsonWebKey (JWK)集,则您可以使用与 OpenID Connect Bearer Token Integration 测试 Wiremock
部分中相同的方法,但只需要更改 application.properties
以使用 MP JWT 配置属性:
keycloak.url is set by OidcWiremockTestResource
# keycloak.url is set by OidcWiremockTestResource
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
Copy to clipboardCopied1.3.9.2. Keycloak
如果您使用 Keycloak,并将 mp.jwt.verify.publickey.location
指向 HTTPS 或基于 HTTP 的 JsonWebKey (JWK)集,您可以使用与 OpenID Connect Bearer Token Integration 测试 部分中描述的相同方法,但只需要更改 application.properties
以使用 MP JWT 配置属性:
keycloak.url is set by DevServices for Keycloak
# keycloak.url is set by DevServices for Keycloak
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
Copy to clipboardCopied
请注意,Keycloak 发布的令牌会将 iss
(issuer)声明设置为 realm 端点地址。
如果您的 Quarkus 应用程序在 Docker 容器中运行,它可能会与由 DevServices for Keycloak 启动的 Keycloak 容器共享一个网络接口。在这种情况下,Quarkus 应用程序和 Keycloak 通过内部共享 Docker 网络进行通信。
在这种情况下,使用以下配置:
keycloak.url is set by DevServices for Keycloak, Quarkus accesses it through an internal shared docker network interface. Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property
# keycloak.url is set by DevServices for Keycloak,
# Quarkus accesses it through an internal shared docker network interface.
mp.jwt.verify.publickey.location=${keycloak.url}/realms/quarkus/protocol/openid-connect/certs
# Issuer is set to the docker bridge localhost endpoint address represented by the `client.quarkus.oidc.auth-server-url` property
mp.jwt.verify.issuer=${client.quarkus.oidc.auth-server-url}
Copy to clipboardCopied1.3.9.3. 本地公钥
您可以使用与 OpenID Connect Bearer Token Integration 测试 本地公钥
部分中所述的方法相同,但仅将 application.properties
更改为使用 MP JWT 配置属性:
mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB # set it to the issuer value which is used to generate the tokens mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus # required to sign the tokens smallrye.jwt.sign.key.location=privateKey.pem
mp.jwt.verify.publickey=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
# set it to the issuer value which is used to generate the tokens
mp.jwt.verify.issuer=${keycloak.url}/realms/quarkus
# required to sign the tokens
smallrye.jwt.sign.key.location=privateKey.pem
Copy to clipboardCopied1.3.9.4. TestSecurity
注解
添加以下依赖项:
使用 Maven:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security-jwt</artifactId> <scope>test</scope> </dependency>
Copy to clipboardCopied<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security-jwt</artifactId> <scope>test</scope> </dependency>
使用 Gradle:
testImplementation("io.quarkus:quarkus-test-security-jwt")
Copy to clipboardCopiedtestImplementation("io.quarkus:quarkus-test-security-jwt")
然后,编写测试代码,如下所示:
import static org.hamcrest.Matchers.is; import org.junit.jupiter.api.Test; import io.quarkus.test.common.http.TestHTTPEndpoint; import io.quarkus.test.junit.QuarkusTest; import io.quarkus.test.security.TestSecurity; import io.quarkus.test.security.jwt.Claim; import io.quarkus.test.security.jwt.JwtSecurity; import io.restassured.RestAssured; @QuarkusTest @TestHTTPEndpoint(ProtectedResource.class) public class TestSecurityAuthTest { @Test @TestSecurity(user = "userJwt", roles = "viewer") public void testJwt() { RestAssured.when().get("test-security-jwt").then() .body(is("userJwt:viewer")); } @Test @TestSecurity(user = "userJwt", roles = "viewer") @JwtSecurity(claims = { @Claim(key = "email", value = "user@gmail.com") }) public void testJwtWithClaims() { RestAssured.when().get("test-security-jwt-claims").then() .body(is("userJwt:viewer:user@gmail.com")); } }
import static org.hamcrest.Matchers.is;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.http.TestHTTPEndpoint;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.security.TestSecurity;
import io.quarkus.test.security.jwt.Claim;
import io.quarkus.test.security.jwt.JwtSecurity;
import io.restassured.RestAssured;
@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {
@Test
@TestSecurity(user = "userJwt", roles = "viewer")
public void testJwt() {
RestAssured.when().get("test-security-jwt").then()
.body(is("userJwt:viewer"));
}
@Test
@TestSecurity(user = "userJwt", roles = "viewer")
@JwtSecurity(claims = {
@Claim(key = "email", value = "user@gmail.com")
})
public void testJwtWithClaims() {
RestAssured.when().get("test-security-jwt-claims").then()
.body(is("userJwt:viewer:user@gmail.com"));
}
}
Copy to clipboardCopied
其中 ProtectedResource
类可能类似如下:
@Path("/web-app") @Authenticated public class ProtectedResource { @Inject JsonWebToken accessToken; @GET @Path("test-security-jwt") public String testSecurityOidc() { return accessToken.getName() + ":" + accessToken.getGroups().iterator().next(); } @GET @Path("test-security-jwt-claims") public String testSecurityOidcUserInfoMetadata() { return accessToken.getName() + ":" + accessToken.getGroups().iterator().next() + ":" + accessToken.getClaim("email"); } }
@Path("/web-app")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
@GET
@Path("test-security-jwt")
public String testSecurityOidc() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
}
@GET
@Path("test-security-jwt-claims")
public String testSecurityOidcUserInfoMetadata() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
+ ":" + accessToken.getClaim("email");
}
}
Copy to clipboardCopied
请注意,必须始终使用 @TestSecurity
注释,并且其 user
属性返回为 JsonWebToken.getName ()
和 roles
属性 - 作为 JsonWebToken.getGroups ()
。@JwtSecurity
注释是可选的,可用于设置额外的令牌声明。
@TestSecurity
和 @JwtSecurity
可以合并到 meta-annotation 中,如下所示:
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) @TestSecurity(user = "userOidc", roles = "viewer") @OidcSecurity(introspectionRequired = true, introspection = { @TokenIntrospection(key = "email", value = "user@gmail.com") } ) public @interface TestSecurityMetaAnnotation { }
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "user@gmail.com")
}
)
public @interface TestSecurityMetaAnnotation {
}
Copy to clipboardCopied如果在多个测试方法中使用同一组安全设置,这特别有用。
1.3.10. 如何检查日志中的错误
请启用 io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator
级别日志记录,以查看令牌验证或解密错误的更多详情:
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".level=TRACE
quarkus.log.category."io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator".min-level=TRACE
Copy to clipboardCopied1.3.11. 主动验证
如果您想在调用公共端点方法时跳过令牌验证,请禁用 主动身份验证。
请注意,如果没有令牌验证,则无法通过公共方法访问注入的 JsonWebToken
。
1.3.12. 如何直接添加 SmallRye JWT
要使用 JWTParser 解析并验证 JsonWebToken,请在以下情况中使用 smallrye-jwt
而不是 quarkus-smallrye-jwt
:
-
您可以使用不支持
HTTP
的 Quarkus 扩展,如Quarkus GRPC
。 -
您可以提供一个特定于扩展的
HTTP
,它的支持与quarkus-smallrye-jwt
和Vert.x HTTP
提供的支持冲突,如Quarkus AWS Lambda
。
从添加 smallrye-jwt
依赖项开始:
使用 Maven:
<dependency> <groupId>io.smallrye</groupId> <artifactId>smallrye-jwt</artifactId> </dependency>
Copy to clipboardCopied<dependency> <groupId>io.smallrye</groupId> <artifactId>smallrye-jwt</artifactId> </dependency>
使用 Gradle:
implementation("io.smallrye:smallrye-jwt")
Copy to clipboardCopiedimplementation("io.smallrye:smallrye-jwt")
然后,更新 application.properties
以获取 smallrye-jwt
提供的所有 CDI 生成者,如下所示:
quarkus.index-dependency.smallrye-jwt.group-id=io.smallrye quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt
quarkus.index-dependency.smallrye-jwt.group-id=io.smallrye
quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt
Copy to clipboardCopied1.4. 配置参考
1.4.1. Quarkus 配置
在构建时修复的配置属性 - 所有其他配置属性在运行时可覆盖
配置属性 | 类型 | default |
MP-JWT 配置对象
环境变量: | 布尔值 |
|
支持 SHA256withRSA 签名的
环境变量: | string |
|
如果获取远程密钥是一个耗时的操作,则启用此属性。如果您使用本地密钥,请不要启用它。
环境变量: | 布尔值 |
|
始终创建 HTTP 401 质询,即使对于不包含身份验证凭据的请求。当需要身份验证时,JWT 身份验证机制将返回 HTTP 401。但是,如果它与其中一个交互式身份验证机制一起使用,那么可能不希望将 HTTP 401 返回到从浏览器访问应用的用户。如果您希望请求 JWT 身份验证机制在这样的情形中不会产生挑战,方法是将此属性设置为"true"。
环境变量: | 布尔值 |
|
1.4.2. MicroProfile JWT 配置
属性名称 | default | 描述 |
---|---|---|
|
|
|
|
|
config 属性允许公钥的指定外部或内部位置。该值可以是相对路径或 URL。如果值指向基于 HTTPS 的 JWK 集,那么对于它以原生模式工作,则 |
|
|
签名算法列表。将它设置为 |
|
| config 属性允许 Private Decryption Key 的指定外部或内部位置。 |
|
|
解密算法列表。把它设置为 |
|
|
config 属性指定服务器接受有效的 JWT 的 |
|
|
以逗号分隔的令牌 |
|
| 在令牌过期和年龄验证过程中使用的时钟偏移(以秒为单位)。如果当前时间在令牌到期时间后此属性指定的秒数内,则接受已过期令牌。默认值为 60 秒。 |
|
|
因为令牌 |
|
|
如果使用另一个标头(如 |
|
|
包含令牌的 Cookie 名称。只有 |
1.4.3. 其他 SmallRye JWT 配置
smallrye JWT 提供了更多属性,可用于自定义令牌处理:
属性名称 | default | 描述 |
---|---|---|
|
| 作为字符串提供的 secret 密钥。 |
|
| 验证密钥的位置,它可以指向公钥和机密密钥。机密密钥只能采用 JWK 格式。请注意,如果设置了此属性,则 'mp.jwt.verify.publickey.location' 会被忽略。 |
|
签名算法.此属性应仅用于设置对称算法,如 | |
|
|
将此属性设置为特定的密钥格式,如 |
|
|
默认情况下,可以使用 PEM、JWK 或 JWK 密钥集从本地文件系统读取或根据 MicroProfile JWT 规范的要求从 URI 获取。将此属性设置为 |
|
|
对验证密钥的验证 ; 将此属性设置为 |
|
| 如果启用了此属性,签名的令牌必须包含 'x5t' 或 'x5t SerialS256' X509Certificate thumbprint 标头。验证密钥只能采用 JWK 或 PEM 证书密钥格式。JWK 密钥必须设置 'x5c'(Base64 编码的 X509Certificate)属性。 |
|
|
如果使用另一个标头(如 |
|
|
密钥缓存大小。当密钥提供程序(如 |
|
|
密钥缓存条目时间(以分钟为单位)当密钥提供程序(如 |
|
|
包含令牌的 Cookie 名称。只有在 |
|
|
即使将 |
|
|
包含替代单一或多个方案(如 |
|
|
键标识符。如果已设置,验证 JWK 密钥和每个 JWT 令牌必须具有匹配的 |
|
| 可以签发 JWT 以供使用的最大秒数。实际上,JWT 和当前发布的过期日期之间的差别不得超过这个值。将此属性设置为非正数值,调整令牌的要求,使其具有有效的"iat" (签发)声明。 |
|
|
如果应用程序依赖于 |
|
|
包含主题名称的声明的路径。它从顶级 JSON 对象开始,并且可以包含多个片段,每个片段仅代表 JSON 对象名称,如 |
|
|
当当前令牌没有标准或自定义子声明可用时,此属性可以设置默认 |
|
|
包含组的声明的路径。它从顶级 JSON 对象开始,可以包含多个片段,每个片段仅代表 JSON 对象名称,例如: |
|
|
分割字符串的分隔符可能包含多个组值。只有在 |
|
| 当当前令牌没有可用的标准或自定义组声明时,此属性可以设置默认 groups 声明值。 |
|
|
JWK 缓存刷新间隔(以分钟为单位)。除非 |
|
|
强制 JWK 缓存刷新间隔(以分钟为单位),用于限制强制刷新尝试的频率,因为缓存没有 JWK 密钥与当前令牌的 |
|
|
过期宽限期(以秒为单位)。默认情况下,如果在令牌到期时间后当前时间不超过 1 分钟,则过期的令牌仍然被接受。此属性已弃用。使用 |
|
|
以逗号分隔的令牌 |
|
| 以逗号分隔的声明列表必须包含。 |
|
|
config 属性来指定 Private Decryption Key 的外部或内部位置。此属性已弃用 - 使用 |
|
| 解密算法. |
|
| 作为字符串提供的解密密钥。 |
|
|
解密密钥标识符.如果设置了,则解密 JWK 密钥以及每个 JWT 令牌必须具有匹配的 |
|
|
如果需要通过 |
|
|
信任所有主机名。如果需要通过 |
|
|
组可信主机名。如果密钥必须通过 |
|
| HTTP 代理主机. |
|
| HTTP 代理端口。 |
|
|
如果 |
|
如果 | |
|
密钥存储密码。如果 | |
|
如果 | |
|
如果 | |
|
如果 | |
|
| 将此属性设置为 true,以在应用启动时解析远程密钥。 |
1.5. 参考
第 2 章 构建、签名和加密 JSON Web 令牌
JSON Web 令牌(JWT)由 RFC 7519 规范定义为代表声明的紧凑 URL 安全方法。这些声明被编码为 JSON 对象,可用作 JSON Web 签名(JWS)结构的有效负载或 JSON Web 加密(JWE)结构的纯文本。这种机制使声明可以进行数字签名或保护,以获得消息身份验证代码(MAC)并加密的完整性。
签名声明是保护声明的最常见方法。通常,通过签名声明以 JSON 格式生成 JWT 令牌,遵循 JSON Web 签名(JWS) 规范中所述的步骤。
当声明包含敏感信息时,可以使用 JSON Web 加密(JWE) 规范来确保其机密。此方法生成带有加密声明的 JWT。
为提高安全性,您可以组合这两种方法:首先对声明进行签名,然后加密生成的嵌套 JWT。此过程可确保声明的机密性和完整性。
SmallRye JWT Build API 通过支持所有这些选项来简化 JWT 声明的安全。它在内部使用 Jose4J 库来提供此功能。
2.1. 依赖项
要使用 SmallRye JWT Build API,请将以下依赖项添加到项目中:
使用 Maven:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
Copy to clipboardCopied<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-smallrye-jwt-build</artifactId> </dependency>
使用 Gradle:
implementation("io.quarkus:quarkus-smallrye-jwt-build")
Copy to clipboardCopiedimplementation("io.quarkus:quarkus-smallrye-jwt-build")
您可以独立使用 SmallRye JWT Build API,而不创建 quarkus-smallrye-jwt
扩展支持的 MicroProfile JWT 端点。
2.2. 创建 JwtClaimsBuilder 并设置声明
第一步是使用以下选项之一初始化 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"); // Alternatively, start with claims directly: // JwtClaimsBuilder builder1 = Jwt.upn("Alice"); // Create a builder from an existing claims file JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json"); // Create a builder from a map of claims JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value")); // Create a builder from a 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); // Create a builder from a JsonWebToken @Inject JsonWebToken token; JwtClaimsBuilder builder5 = Jwt.claims(token);
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");
// Alternatively, start with claims directly:
// JwtClaimsBuilder builder1 = Jwt.upn("Alice");
// Create a builder from an existing claims file
JwtClaimsBuilder builder2 = Jwt.claims("/tokenClaims.json");
// Create a builder from a map of claims
JwtClaimsBuilder builder3 = Jwt.claims(Collections.singletonMap("customClaim", "custom-value"));
// Create a builder from a 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);
// Create a builder from a JsonWebToken
@Inject JsonWebToken token;
JwtClaimsBuilder builder5 = Jwt.claims(token);
Copy to clipboardCopiedAPI 非常流畅,以便您可以将构建器初始化为流畅序列的一部分。
如果没有明确配置,构建程序会自动设置以下声明:
-
iat
(在以下位置发出):当前时间 -
exp
(expires at):自当前时间起的五分钟(可定制为smallrye.jwt.new-token.lifespan
属性) -
jti
(唯一令牌标识符)
您可以在全局范围内配置以下属性,以避免直接在构建器中设置它们:
-
smallrye.jwt.new-token.issuer
: 指定默认签发者。 -
smallrye.jwt.new-token.audience
: 指定默认受众。
初始化和设置声明后,下一步是决定如何保护声明。
2.3. 为声明签名
您可以立即或在配置 JSON Web 签名(JWS)标头后为声明签名
:
import io.smallrye.jwt.build.Jwt; ... // Sign the claims using an RSA private key loaded from the location specified by the 'smallrye.jwt.sign.key.location' property. // No 'jws()' transition is required. The default algorithm is RS256. String jwt1 = Jwt.claims("/tokenClaims.json").sign(); // Set the headers and sign the claims by using an RSA private key loaded in the code (the implementation of this method is omitted). // Includes a 'jws()' transition to a 'JwtSignatureBuilder'. The default algorithm is RS256. String jwt2 = Jwt.claims("/tokenClaims.json") .jws() .keyId("kid1") .header("custom-header", "custom-value") .sign(getPrivateKey());
import io.smallrye.jwt.build.Jwt;
...
// Sign the claims using an RSA private key loaded from the location specified by the 'smallrye.jwt.sign.key.location' property.
// No 'jws()' transition is required. The default algorithm is RS256.
String jwt1 = Jwt.claims("/tokenClaims.json").sign();
// Set the headers and sign the claims by using an RSA private key loaded in the code (the implementation of this method is omitted).
// Includes a 'jws()' transition to a 'JwtSignatureBuilder'. The default algorithm is RS256.
String jwt2 = Jwt.claims("/tokenClaims.json")
.jws()
.keyId("kid1")
.header("custom-header", "custom-value")
.sign(getPrivateKey());
Copy to clipboardCopied默认行为:
-
默认情况下
,
alg (算法)标头设置为RS256
。 -
如果使用包含
kid
属性的单个 JSON Web Key (JWK),则不必设置签名密钥标识符(kid
标头)。
支持的密钥和算法:
- 要签署声明,您可以使用 RSA 私钥、Eliptic Curve (EC)私钥和对称 secret 密钥。
-
RS256
是默认的 RSA 私钥签名算法。 -
ES256
是默认的 EC 私钥签名算法。 -
HS256
是默认的对称密钥签名算法。
若要自定义签名算法,可使用 JwtSignatureBuilder
API。例如:
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. The algorithm is PS256. String jwt = Jwt.upn("Alice").jws().algorithm(SignatureAlgorithm.PS256).sign();
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. The algorithm is PS256.
String jwt = Jwt.upn("Alice").jws().algorithm(SignatureAlgorithm.PS256).sign();
Copy to clipboardCopied另外,您可以使用以下属性全局配置签名算法:
smallrye.jwt.new-token.signature-algorithm=PS256
smallrye.jwt.new-token.signature-algorithm=PS256
Copy to clipboardCopied这个方法为您提供了一个简单的 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. The algorithm is PS256. String jwt = Jwt.upn("Alice").sign();
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. The algorithm is PS256.
String jwt = Jwt.upn("Alice").sign();
Copy to clipboardCopied
您可以将 签名步骤与
加密 步骤相结合,以创建 内部签名和加密
的令牌。如需更多信息,请参阅 签名声明并加密嵌套的 JWT 令牌 部分。
2.4. 加密声明
您可以立即加密声明,或者在设置 JSON Web 加密(JWE)
标头后加密声明,类似于如何签署声明。但是,加密声明始终需要一个 jwe ()
转换为 JwtEncryptionBuilder
,因为 API 被优化以支持签名和内部签名操作。
import io.smallrye.jwt.build.Jwt; ... // Encrypt the claims using an RSA public key loaded from the location specified by the 'smallrye.jwt.encrypt.key.location' property. // The default key encryption algorithm is RSA-OAEP. String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt(); // Set the headers and encrypt the claims by using an RSA public key loaded in the code (the implementation of this method is omitted). // The default key encryption algorithm is A256KW. String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getSecretKey());
import io.smallrye.jwt.build.Jwt;
...
// Encrypt the claims using an RSA public key loaded from the location specified by the 'smallrye.jwt.encrypt.key.location' property.
// The default key encryption algorithm is RSA-OAEP.
String jwt1 = Jwt.claims("/tokenClaims.json").jwe().encrypt();
// Set the headers and encrypt the claims by using an RSA public key loaded in the code (the implementation of this method is omitted).
// The default key encryption algorithm is A256KW.
String jwt2 = Jwt.claims("/tokenClaims.json").jwe().header("custom-header", "custom-value").encrypt(getSecretKey());
Copy to clipboardCopied默认行为:
-
alg
(密钥管理算法)标头默认为RSA-OAEP
。 -
enc
(内容加密)标头默认为A256GCM
。
支持的密钥和算法:
- 您可以使用 RSA 公钥、Elliptic Curve (EC)公钥和对称 secret 密钥来加密声明。
-
RSA-OAEP
是默认的 RSA 公钥加密算法。 -
ECDH-ES
是默认的 EC 公钥加密算法。 -
A256KW
是默认的对称密钥加密算法。
请注意,在创建加密令牌时执行两个加密操作:
-
生成的内容加密密钥是使用提供的密钥和密钥加密算法(如
RSA-OAEP
)进行加密。 -
该声明使用内容加密密钥和内容加密算法(如
A256GCM
)进行加密。
您可以使用 JwtEncryptionBuilder
API 自定义密钥和证书算法。例如:
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. The content encryption algorithm is A256CBC-HS512. String jwt = Jwt.subject("Bob").jwe() .keyAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP_256) .contentAlgorithm(ContentEncryptionAlgorithm.A256CBC_HS512) .encrypt();
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. The content encryption algorithm is A256CBC-HS512.
String jwt = Jwt.subject("Bob").jwe()
.keyAlgorithm(KeyEncryptionAlgorithm.RSA_OAEP_256)
.contentAlgorithm(ContentEncryptionAlgorithm.A256CBC_HS512)
.encrypt();
Copy to clipboardCopied另外,您可以使用以下属性全局配置算法:
smallrye.jwt.new-token.key-encryption-algorithm=RSA-OAEP-256 smallrye.jwt.new-token.content-encryption-algorithm=A256CBC-HS512
smallrye.jwt.new-token.key-encryption-algorithm=RSA-OAEP-256
smallrye.jwt.new-token.content-encryption-algorithm=A256CBC-HS512
Copy to clipboardCopied此配置允许更简单的 API 序列:
import io.smallrye.jwt.build.Jwt; // Encrypt the claims by 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. The content encryption algorithm is A256CBC-HS512. String jwt = Jwt.subject("Bob").encrypt();
import io.smallrye.jwt.build.Jwt;
// Encrypt the claims by 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. The content encryption algorithm is A256CBC-HS512.
String jwt = Jwt.subject("Bob").encrypt();
Copy to clipboardCopied安全令牌加密建议:
- 当令牌直接使用公共 RSA 或 EC 密钥加密时,无法验证哪个方发送令牌。为解决此问题,最好使用对称 secret 密钥进行直接加密,特别是在将 JWT 用作 Cookie 仅由 Quarkus 端点管理时。
- 要使用 RSA 或 EC 公钥加密令牌,如果有签名密钥,则建议首先为令牌签名。如需更多信息,请参阅 签名声明并加密嵌套的 JWT 令牌 部分。
2.5. 为声明签名并加密嵌套的 JWT 令牌
您可以签署声明,然后通过组合签名和加密步骤来加密嵌套的 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 // specified by the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties, respectively. // The signature algorithm is RS256, and the key encryption algorithm is RSA-OAEP-256. String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt();
import io.smallrye.jwt.build.Jwt;
...
// Sign the claims and encrypt the nested token using the private and public keys loaded from the locations
// specified by the 'smallrye.jwt.sign.key.location' and 'smallrye.jwt.encrypt.key.location' properties, respectively.
// The signature algorithm is RS256, and the key encryption algorithm is RSA-OAEP-256.
String jwt = Jwt.claims("/tokenClaims.json").innerSign().encrypt();
Copy to clipboardCopied2.6. 快速 JWT 生成
如果设置了 smallrye.jwt.sign.key.location
或 smallrye.jwt.encrypt.key.location
属性,您可以使用单个调用来保护现有的声明,如 resources, maps, 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");
// 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");
Copy to clipboardCopied
如前文所述,如果尚未设置,则会自动添加以下声明: iat
(issued at), exp
(expires at), jti
(token identifier), iss
(issuer)和 aud
(audience)。
2.7. 处理密钥
您可以使用 smallrye.jwt.sign.key.location
和 smallrye.jwt.encrypt.key.location
属性来指定签名和加密密钥的位置。这些密钥可以位于本地文件系统上,位于 classpath 上,或者从远程端点获取。密钥可以是 PEM
或 JSON Web 密钥(JWK)
格式。例如:
smallrye.jwt.sign.key.location=privateKey.pem smallrye.jwt.encrypt.key.location=publicKey.pem
smallrye.jwt.sign.key.location=privateKey.pem
smallrye.jwt.encrypt.key.location=publicKey.pem
Copy to clipboardCopied
或者,您可以使用 MicroProfile ConfigSource
和 smallrye.jwt.sign.key
和 smallrye.jwt.encrypt.key
属性从外部服务获取密钥,如 HashiCorp Vault 或其他 secret 管理器:
smallrye.jwt.sign.key=${private.key.from.vault} smallrye.jwt.encrypt.key=${public.key.from.vault}
smallrye.jwt.sign.key=${private.key.from.vault}
smallrye.jwt.encrypt.key=${public.key.from.vault}
Copy to clipboardCopied
在本例中,private.key.from.vault
和 public.key.from.vault
是 PEM
或 JWK
格式的密钥值,由自定义 ConfigSource
提供。
smallrye.jwt.sign.key
和 smallrye.jwt.encrypt.key
属性也可以直接包含 base64 编码的私钥或公钥值。
但请注意,不建议在配置中直接显示私钥。仅在需要从远程 secret manager 获取签名密钥值时,才使用 smallrye.jwt.sign.key
属性。
密钥也可以由构建令牌的代码加载,然后提供给 JWT Build API 以进行令牌创建。
如果您需要使用对称 secret 密钥签名或加密令牌,请考虑使用 io.smallrye.jwt.util.KeyUtils
生成所需长度的 SecretKey
。
例如,需要使用 HS512
算法(512/8
)为令牌签名 64 字节密钥,需要使用 A256KW
算法加密内容加密密钥(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);
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);
Copy to clipboardCopied
您还可以考虑使用 JSON Web 密钥(JWK)或 JSON Web 密钥集(JWK Set)格式将 secret 密钥存储在安全文件系统中。您可以使用 smallrye.jwt.sign.key.location
或 smallrye.jwt.encrypt.key.location
属性来引用密钥。
JWK 示例
{ "kty":"oct", "kid":"secretKey", "k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I" }
{
"kty":"oct",
"kid":"secretKey",
"k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"
}
Copy to clipboardCopiedJWK 设置示例
{ "keys": [ { "kty":"oct", "kid":"secretKey1", "k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I" }, { "kty":"oct", "kid":"secretKey2", "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow" } ] }
{
"keys": [
{
"kty":"oct",
"kid":"secretKey1",
"k":"Fdh9u8rINxfivbrianbbVT1u232VQBZYKx1HGAGPt2I"
},
{
"kty":"oct",
"kid":"secretKey2",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
]
}
Copy to clipboardCopied
您还可以使用 io.smallrye.jwt.util.KeyUtils
生成一对非对称 RSA 或 EC 密钥。这些密钥可以存储 JWK
、JWK Set
或 PEM
格式。
2.8. smallrye JWT Builder 配置
smallrye JWT 支持以下属性,可用于自定义声明如何签名或加密:
属性名称 | default | 描述 |
---|---|---|
|
|
当调用 no-argument 符号 |
|
|
当调用 no-argument |
|
| 签名密钥标识符,仅在使用 JWK 密钥时检查。 |
|
|
调用 no-argument |
|
| 对签名密钥的验证进行 Relax 操作。 |
|
|
调用 no-argument |
|
| 加密密钥标识符,仅在使用 JWK 密钥时检查。 |
|
| 对加密密钥的验证进行 Relax 操作。 |
|
| 签名算法.检查 JWT 签名构建器尚未设置签名算法。 |
|
| 密钥加密算法。检查 JWT 加密构建器尚未设置密钥加密算法。 |
|
| 内容加密算法。检查 JWT 加密构建器尚未设置内容加密算法。 |
|
|
如果尚未设置此声明,令牌生命周期(以秒为单位)用于计算 |
|
|
如果尚未设置此声明,用于设置 |
|
|
如果尚未设置此声明,用于设置 |
|
|
对于 |
|
|
如果 |
|
如果 | |
|
密钥存储密码。如果 | |
|
如果 | |
|
如果 | |
|
如果 |