使用 JWT RBAC
本指南介绍了如何将 SmallRye JWT 集成到您的 Quarkus 应用程序中,以实现符合 MicroProfile JWT 规范的 JSON Web Token (JWT) 安全。您将学习如何验证 JWT、将其表示为 MicroProfile JWT org.eclipse.microprofile.jwt.JsonWebToken
,以及如何使用 Bearer 令牌授权和 基于角色的访问控制 来保护 Quarkus HTTP 端点。
Quarkus OpenID Connect ( 如果您的 Quarkus 应用程序需要使用 OIDC 授权码流来认证用户,则必须使用 OpenID Connect 扩展。有关更多信息,请参阅 保护 Web 应用程序的 OIDC 授权码流机制。 |
先决条件
要完成本指南,您需要
-
大约 15 分钟
-
一个 IDE
-
已安装 JDK 17+ 并正确配置了
JAVA_HOME
-
Apache Maven 3.9.9
-
如果您想使用它,可以选择 Quarkus CLI
-
如果您想构建本机可执行文件(或者如果您使用本机容器构建,则为 Docker),可以选择安装 Mandrel 或 GraalVM 并进行适当的配置
快速入门
解决方案
我们建议按照后续部分的说明逐步创建应用程序。如果您愿意,可以跳到已完成的示例。
要访问示例,请克隆 Git 存储库或下载存档
-
克隆存储库:
git clone https://github.com/quarkusio/quarkus-quickstarts.git
。 -
下载 存档。
已完成的解决方案位于 security-jwt-quickstart
目录中。
创建 Maven 项目
首先,使用以下命令创建一个新项目
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数括在双引号中,例如"-DprojectArtifactId=security-jwt-quickstart"
此命令会生成 Maven 项目并导入 smallrye-jwt
扩展,其中包含 MicroProfile JWT RBAC 支持。
如果您已经配置了 Quarkus 项目,可以通过在项目根目录下运行以下命令将 smallrye-jwt
扩展添加到项目中
quarkus extension add smallrye-jwt,smallrye-jwt-build
./mvnw quarkus:add-extension -Dextensions='smallrye-jwt,smallrye-jwt-build'
./gradlew addExtension --extensions='smallrye-jwt,smallrye-jwt-build'
此命令将以下依赖项添加到您的构建文件中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-jwt-build</artifactId>
</dependency>
implementation("io.quarkus:quarkus-smallrye-jwt")
implementation("io.quarkus:quarkus-smallrye-jwt-build")
检查 Jakarta REST 资源
在 src/main/java/org/acme/security/jwt/TokenSecuredResource.java
中创建一个 REST 端点,内容如下
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;
}
}
1 | 注入 JsonWebToken 接口,提供对当前已验证令牌相关联的声明的访问。此接口扩展了 java.security.Principal 。 |
2 | @PermitAll 是一个标准的 Jakarta 安全注解。它表示给定端点可供所有调用者访问,无论是否已验证。 |
3 | 注入 Jakarta REST SecurityContext 以检查请求的安全状态。getResponseString() 函数生成响应。 |
4 | 通过检查请求用户/调用者 Principal 是否为 null 来检查调用是否不安全。 |
5 | 确保 Principal 和 JsonWebToken 中的名称匹配,因为 JsonWebToken 代表当前的 Principal 。 |
6 | 检索 Principal 的名称。 |
7 | 构建一个包含调用者姓名、请求 SecurityContext 的 isSecure() 和 getAuthenticationScheme() 状态,以及是否注入了非 null JsonWebToken 的响应。 |
以开发模式运行应用程序
现在,您可以使用以下任一命令以开发模式运行应用程序
quarkus dev
./mvnw quarkus:dev
./gradlew --console=plain quarkusDev
然后,您应该看到类似以下示例的输出
quarkus:dev
输出[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]
现在 REST 端点正在运行,您可以使用 curl 等命令行工具访问它
curl
命令用于 /secured/permit-all
$ curl http://127.0.0.1:8080/secured/permit-all; echo
此命令返回以下响应
hello anonymous, isHttps: false, authScheme: null, hasJWT: false
您在请求中没有提供任何 JWT,因此您不应期望端点看到任何安全状态,并且响应与此一致
-
username
为匿名。 -
isHttps
为false
,因为未使用https
。 -
authScheme
为null
。 -
hasJWT
为false
。
使用 Ctrl-C 停止 Quarkus 服务器。
现在让我们来保护一些东西。看一下新的端点方法 helloRolesAllowed
,如下所示
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;
}
}
1 | 注入 JsonWebToken 以访问 JWT 中的声明。 |
2 | 此端点暴露在 /secured/roles-allowed 。 |
3 | @RolesAllowed 注解将访问限制为具有 "User" 或 "Admin" 角色的用户。 |
4 | 响应的构造与 hello 方法类似,此外还添加了直接从注入的 JsonWebToken 中检索的 birthdate 声明。 |
在将此添加到 TokenSecuredResource
后,请重新运行 ./mvnw quarkus:dev
命令,然后尝试 curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
来尝试访问新端点。
您的输出应如下所示
curl
命令用于 /secured/roles-allowed
$ curl -v http://127.0.0.1:8080/secured/roles-allowed; echo
此命令返回以下响应
* 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
太好了。您没有在请求中提供 JWT,因此正确拒绝了对该端点的访问。相反,您收到了 HTTP 401 Unauthorized 错误。
要访问该端点,您必须获取一个有效的 JWT 并将其包含在您的请求中。这涉及两个步骤
-
使用必要的 JWT 验证信息配置 SmallRye JWT 扩展。
-
生成一个具有适当声明的 JWT 以匹配配置。
配置 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)
1 | 指定类路径上公钥文件 publicKey.pem 的位置。有关添加此密钥,请参阅 添加公钥。 |
2 | 将预期的发布者定义为 https://example.com/issuer 。 |
3 | 确保 publicKey.pem 文件作为资源包含在原生可执行文件中。 |
添加公钥
JWT 规范定义了 JWT 的各种安全级别。MicroProfile JWT RBAC 规范要求使用 RSA-256 签名算法签名的 JWT。这反过来又需要 RSA 公钥对。在 REST 端点服务器端,您需要配置用于验证请求中发送的 JWT 的 RSA 公钥的位置。先前配置的 mp.jwt.verify.publickey.location=publicKey.pem
设置期望公钥作为 publicKey.pem
在类路径上可用。为此,请将以下内容复制到 security-jwt-quickstart/src/main/resources/publicKey.pem
文件中。
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEq
Fyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwR
TYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5e
UF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9
AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYn
sIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9x
nQIDAQAB
-----END PUBLIC KEY-----
生成 JWT
通常,JWT 从身份管理器(如 Keycloak)获取。但对于这个快速入门,我们将使用 smallrye-jwt
提供的 JWT 生成 API 来生成我们自己的 JWT。有关更多信息,请参阅 使用 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);
}
}
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
中
-----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-----
稍后,您将配置 smallrye.jwt.sign.key.location
属性来指定私钥签名位置。
使用 OpenSSL 生成密钥
也可以使用 OpenSSL 命令行工具生成公钥和私钥对。 openssl 命令生成密钥
需要一个额外的步骤来生成并将私钥转换为 PKCS#8 格式,这通常用于安全密钥存储和传输。 openssl 命令执行转换
您可以使用生成的密钥对而不是此快速入门中使用的密钥对。 |
在生成 TokenSecuredResource
端点的 JSON Web Token (JWT) 之前,请确保 应用程序正在运行。
接下来,使用以下命令生成 JWT
$ mvn exec:java -Dexec.mainClass=org.acme.security.jwt.GenerateToken -Dexec.classpathScope=test -Dsmallrye.jwt.sign.key.location=privateKey.pem
JWT 字符串是一个 Base64 URL 编码的字符串,包含三个部分,由 .
字符分隔
-
头部,包含有关令牌的元数据,例如签名算法。
-
载荷,也称为“声明”,包括令牌的声明或数据。
-
签名,用于验证令牌的完整性。
最后,安全访问 /secured/roles-allowed
现在,让我们使用它来向 /secured/roles-allowed
端点发出安全请求。确保 Quarkus 服务器仍在开发模式下运行,然后运行以下命令,确保使用上一步生成的 JWT 的版本
/secured/roles-allowed
的 curl
命令$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed; echo
请务必使用生成的令牌作为 HTTP Authorization Bearer 方案的值。
此命令返回以下响应
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
成功!您现在拥有以下内容
-
一个非匿名的调用者名称:
jdoe@quarkus.io
-
一个身份验证方案:
Bearer
-
一个非 null 的
JsonWebToken
-
birthdate
声明值
使用 JsonWebToken
和声明注入
既然您可以生成 JWT 来访问我们安全的 REST 端点,那么让我们看看您还可以使用 JsonWebToken
接口和 JWT 声明做些什么。org.eclipse.microprofile.jwt.JsonWebToken
接口扩展了 java.security.Principal
接口,并且是您之前使用的 jakarta.ws.rs.core.SecurityContext#getUserPrincipal()
调用返回的对象类型。这意味着不使用 CDI 但可以访问 REST 容器 SecurityContext
的代码可以通过强制转换 SecurityContext#getUserPrincipal()
来获取调用者的 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;
}
}
1 | @RequestScoped 范围是必需的,以便将 birthdate 声明作为 String 注入。 |
2 | 在此处注入 JsonWebToken ,提供对所有声明和 JWT 相关信息的访问。 |
3 | birthdate 声明作为 String 注入。这说明了为什么 @RequestScoped 范围是强制性的。 |
4 | 注入的 birthdate 声明直接用于构建响应。 |
现在再次生成令牌并运行
$ curl -H "Authorization: Bearer eyJraWQ..." http://127.0.0.1:8080/secured/roles-allowed-admin; echo
请务必使用生成的令牌作为 HTTP Authorization Bearer 方案的值。
此命令返回以下响应
hello jdoe@quarkus.io, isHttps: false, authScheme: Bearer, hasJWT: true, birthdate: 2001-07-13
以 JVM 模式运行应用程序
您可以将应用程序作为标准 Java 应用程序运行。
-
编译应用程序
CLIquarkus build
Maven./mvnw install
Gradle./gradlew build
-
运行应用程序
java -jar target/quarkus-app/quarkus-run.jar
在原生模式下运行应用程序
您可以在不进行任何修改的情况下将相同的演示编译为原生模式。这意味着您不再需要在生产环境中安装 JVM。运行时技术包含在生成的二进制文件中,并经过优化,以最少的资源运行。
编译需要一段时间,因此默认情况下禁用此步骤。
-
通过启用
native
配置文件再次构建您的应用程序CLIquarkus build --native
Maven./mvnw install -Dnative
Gradle./gradlew build -Dquarkus.native.enabled=true
-
直接运行以下二进制文件
./target/security-jwt-quickstart-1.0.0-SNAPSHOT-runner
探索解决方案
security-jwt-quickstart
目录存储库包含本快速入门指南中涵盖的所有版本,以及演示使用注入的 JsonWebToken
令牌及其通过 CDI API 的声明的子端点。
我们鼓励您探索 security-jwt-quickstart
目录并查看快速入门解决方案,以了解更多关于 SmallRye JWT 扩展的功能。
参考指南
支持的注入范围
当注入 org.eclipse.microprofile.jwt.JsonWebToken
时,支持 @ApplicationScoped
、@Singleton
和 @RequestScoped
外部 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;
}
请注意,您也可以使用注入的 JsonWebToken
来访问单个声明,但在这种情况下,设置 @RequestScoped
是不必要的。
有关更多详细信息,请参阅 MP JWT CDI 注入要求。
支持的公钥格式
公钥可以采用以下任何格式,按优先级顺序排列
-
公钥加密标准 #8 (PKCS#8) PEM
-
JSON Web Key (JWK)
-
JSON Web Key Set (JWKS)
-
JSON Web Key (JWK) Base64 URL 编码
-
JSON Web Key Set (JWKS) Base64 URL 编码
处理验证密钥
如果您需要使用非对称 RSA 或椭圆曲线 (EC) 密钥来验证令牌签名,请使用 mp.jwt.verify.publickey.location
属性来引用本地或远程验证密钥。
使用 mp.jwt.verify.publickey.algorithm
来自定义验证算法(默认为 RS256
);例如,在使用 EC 密钥时将其设置为 ES256
。
如果您需要使用对称密钥来验证令牌签名,那么必须使用 JSON Web Key
(JWK) 或 JSON Web Key Set
(JWK Set) 格式来表示此密钥,例如
{
"keys": [
{
"kty":"oct",
"kid":"secretKey",
"k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow"
}
]
}
此密钥 JWK 也必须使用 smallrye.jwt.verify.key.location
来引用。smallrye.jwt.verify.algorithm
应设置为 HS256
/HS384
/HS512
。
使用 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);
您还可以使用它来定制令牌的验证或解密方式。例如,可以提供一个本地 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();
}
}
}
另请参阅 如何直接添加 SmallRye JWT 部分,了解如何在不使用 quarkus-smallrye-jwt
提供的 HTTP
支持的情况下使用 JWTParser
。
令牌解密
如果您的应用程序需要接受带有加密声明或加密内部签名声明的令牌,只需将 smallrye.jwt.decrypt.key.location
属性设置为指向解密密钥。
如果这是唯一设置的密钥属性,则传入的令牌应仅包含加密声明。如果还设置了 mp.jwt.verify.publickey
或 mp.jwt.verify.publickey.location
验证属性,则传入的令牌应包含加密的内部签名令牌。
请参阅 使用 SmallRye JWT 生成 JWT 令牌,了解如何快速生成加密的或内部签名然后加密的令牌。
自定义工厂
io.smallrye.jwt.auth.principal.DefaultJWTCallerPrincipalFactory
是用于解析和验证 JWT 令牌,将其转换为 JsonWebToken
principal 的默认实现。此工厂依赖于 MP JWT
和 smallrye-jwt
属性,如“配置”部分所述,用于验证和自定义 JWT 令牌。
如果您需要实现自定义工厂(例如,跳过已由防火墙验证的令牌的重新验证),可以通过以下方式之一进行操作
-
使用
ServiceLoader
机制,通过创建META-INF/services/io.smallrye.jwt.auth.principal.JWTCallerPrincipalFactory
资源。 -
提供一个
Alternative
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());
}
}
}
阻塞调用
quarkus-smallrye-jwt
扩展使用 SmallRye JWT 库,该库目前不是响应式的。
从 quarkus-smallrye-jwt
的角度来看,它作为反应式 Quarkus 安全架构的一部分运行,这意味着进入 SmallRye JWT 验证或解密代码的 IO 线程可能在以下任一情况下被阻塞
-
默认密钥解析器刷新包含密钥的
JsonWebKey
集合,这涉及对 OIDC 端点的远程调用。 -
自定义密钥解析器,例如
AWS Application Load Balancer
(ALB
) 密钥解析器,通过使用当前令牌的密钥标识符标头值,针对 AWS ALB 密钥端点解析密钥。
在这些情况下,如果连接速度慢(例如,响应密钥端点需要超过 3 秒),则当前的事件循环线程很可能会被阻塞。
为防止阻塞,请设置 quarkus.smallrye-jwt.blocking-authentication=true
。
令牌传播
有关 Bearer 访问令牌传播到下游服务的信息,请参阅 令牌传播 部分。
测试
自动密钥生成
如果未配置验证密钥,此扩展将在开发和测试模式下生成一个非对称 RSA 2024 位签名密钥对。生成密钥对后,将使用 RSA 公钥配置 mp.jwt.verify.publickey
属性,并且 RSA 私钥可用于测试,例如使用 smallrye-jwt-build
对令牌进行签名
import io.smallrye.jwt.build.Jwt;
import jakarta.ws.rs.GET;
@GET
public String token() {
return Jwt.upn("Alice").sign();
}
有关详细模式,请参阅 签名声明指南。
您可以通过设置以下至少一个属性来禁用自动密钥生成
-
mp.jwt.verify.publickey.location
-
mp.jwt.verify.publickey
-
mp.jwt.decrypt.key.location
-
smallrye.jwt.encrypt.key.location
-
smallrye.jwt.sign.key.location
-
smallrye.jwt.sign.key
在 开发 模式下,如果您不使用 如果您希望以编程方式配置发布者,请将 |
Wiremock
如果您将 mp.jwt.verify.publickey.location
配置为指向基于 HTTPS 或 HTTP 的 JsonWebKey (JWK) 集合,那么您可以使用与 OpenID Connect Bearer 令牌集成测试 Wiremock
部分中描述的相同方法,但只需将 application.properties
更改为使用 MP JWT 配置属性即可。
# 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
Keycloak
如果您使用 Keycloak 并将 mp.jwt.verify.publickey.location
配置为指向基于 HTTPS 或 HTTP 的 JsonWebKey (JWK) 集合,那么您可以使用与 OpenID Connect Bearer 令牌集成测试 Keycloak 部分中描述的相同方法,但只需将 application.properties
更改为使用 MP JWT 配置属性即可。
# 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
请注意,Keycloak 发行的令牌具有设置为领域端点地址的 iss
(issuer) 声明。
如果您的 Quarkus 应用程序在 Docker 容器中运行,它可能与为 Keycloak 启动的 DevServices 启动的 Keycloak 容器共享网络接口。在这种情况下,Quarkus 应用程序和 Keycloak 通过内部共享的 Docker 网络进行通信。
在这种情况下,请使用以下配置代替
# 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}
本地公钥
您可以采用与 OpenID Connect Bearer 令牌集成测试 本地公钥
部分中描述的相同方法,但只需将 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
TestSecurity
注解
添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-jwt</artifactId>
<scope>test</scope>
</dependency>
testImplementation("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"));
}
}
其中 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");
}
}
请注意,@TestSecurity
注解必须始终使用,并且其 user
属性将作为 JsonWebToken.getName()
返回,roles
属性则作为 JsonWebToken.getGroups()
返回。@JwtSecurity
注解是可选的,可用于设置其他令牌声明。
这对于在多个测试方法中使用相同的安全设置特别有用。 |
如何检查日志中的错误
请启用 io.quarkus.smallrye.jwt.runtime.auth.MpJwtValidator
的 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
如何直接添加 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
依赖项开始
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-jwt</artifactId>
</dependency>
implementation("io.smallrye:smallrye-jwt")
然后,更新 application.properties
以包含 smallrye-jwt
提供的所有 CDI producer,如下所示
quarkus.index-dependency.smallrye-jwt.group-id=io.smallrye
quarkus.index-dependency.smallrye-jwt.artifact-id=smallrye-jwt
配置参考
Quarkus 配置
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
布尔值 |
|
|
支持 SHA256withRSA 签名的 环境变量: 显示更多 |
字符串 |
|
如果获取远程密钥可能是一项耗时的操作,则启用此属性。如果使用本地密钥,请勿启用。 环境变量: 显示更多 |
布尔值 |
|
始终创建 HTTP 401 挑战,即使对于不包含身份验证凭据的请求也是如此。JWT 身份验证机制在需要身份验证挑战时将返回 HTTP 401。但是,如果它与交互式身份验证机制之一一起使用,那么向从浏览器访问应用程序的用户返回 HTTP 401 可能是不理想的。如果您愿意,可以通过将此属性设置为“true”来请求 JWT 身份验证机制在这种情况下不创建挑战。 环境变量: 显示更多 |
布尔值 |
|
MicroProfile JWT 配置
属性名称 | 默认 | 描述 |
---|---|---|
|
|
|
|
|
配置属性允许指定公钥的外部或内部位置。该值可以是相对路径或 URL。如果该值指向基于 HTTPS 的 JWK 集合,那么为了在原生模式下工作,必须还将 |
|
|
签名算法列表。将其设置为 |
|
|
配置属性允许指定私钥解密的位置,可以是外部或内部。 |
|
|
解密算法列表。将其设置为 |
|
|
配置属性指定服务器接受为有效的 JWT 的 |
|
|
令牌 |
|
|
用于令牌过期和年龄验证的时钟偏差(以秒为单位)。如果当前时间在令牌过期时间之后的指定秒数之内,则接受已过期令牌。默认值为 60 秒。 |
|
|
令牌 |
|
|
如果使用其他标头(如 |
|
|
包含令牌的 Cookie 名称。仅当 |
其他 SmallRye JWT 配置
SmallRye JWT 提供更多可用于自定义令牌处理的属性
属性名称 | 默认 | 描述 |
---|---|---|
|
|
作为字符串提供的密钥。 |
|
|
验证密钥的位置,可以指向公钥和私钥。私钥只能是 JWK 格式。请注意,如果设置了此属性,则会忽略 'mp.jwt.verify.publickey.location'。 |
|
签名算法。此属性仅应用于设置对称算法(如 |
|
|
|
将此属性设置为特定密钥格式(如 |
|
|
默认情况下,PEM、JWK 或 JWK 密钥集可以从本地文件系统读取,也可以根据 MicroProfile JWT 规范的要求从 URI 获取。将此属性设置为 |
|
|
放宽验证密钥的验证;将此属性设置为 |
|
|
如果启用了此属性,则签名令牌必须包含 'x5t' 或 'x5t#S256' X509Certificate 指纹标头。验证密钥只能是 JWK 或 PEM 证书密钥格式。JWK 密钥必须设置 'x5c' (Base64 编码的 X509Certificate) 属性。 |
|
|
如果使用其他标头(如 |
|
|
密钥缓存大小。当密钥提供程序(如 |
|
|
密钥缓存条目的生存时间(分钟)。当密钥提供程序(如 |
|
|
包含令牌的 Cookie 名称。仅当 |
|
|
将此属性设置为 |
|
|
包含备用单个或多个方案(如 |
|
|
密钥标识符。如果设置了验证 JWK 密钥和每个 JWT 令牌,则必须具有匹配的 |
|
|
JWT 可以签发使用的最大秒数。实际上,JWT 的过期日期与签发日期之间的差值不得超过此值。将此属性设置为非正值会放宽令牌必须具有有效的 'iat' (issued at) 声明的要求。 |
|
|
如果应用程序依赖于 |
|
|
指向包含主题名称的声明的路径。它从顶级 JSON 对象开始,可以包含多个段,每个段仅代表一个 JSON 对象名称,例如: |
|
|
当当前令牌没有可用的标准或自定义 |
|
|
指向包含组的声明的路径。它从顶级 JSON 对象开始,可以包含多个段,每个段仅代表一个 JSON 对象名称,例如: |
|
|
用于分割可能包含多个组值的字符串的分隔符。仅当 |
|
|
当当前令牌没有可用的标准或自定义组声明时,此属性可以设置默认的组声明值。 |
|
|
JWK 缓存刷新间隔(分钟)。除非 |
|
|
强制 JWK 缓存刷新间隔(分钟),用于限制强制刷新尝试的频率,当由于缓存没有与当前令牌的 |
|
|
过期宽限期(秒)。默认情况下,如果当前时间在令牌过期时间后不超过 1 分钟,则仍接受已过期令牌。此属性已弃用。请改用 |
|
|
令牌 |
|
|
令牌必须包含的声明的逗号分隔列表。 |
|
|
用于指定私钥解密位置(外部或内部)的配置属性。此属性已弃用 - 请使用 |
|
|
解密算法。 |
|
|
作为字符串提供的解密密钥。 |
|
|
解密密钥标识符。如果设置了此属性,则解密 JWK 密钥和每个 JWT 令牌都必须具有匹配的 |
|
|
TLS 受信任证书的路径,如果必须通过 |
|
|
信任所有主机名。如果必须通过 |
|
|
受信任的主机名集合。如果必须通过 |
|
|
HTTP 代理主机。 |
|
|
HTTP 代理端口。 |
|
|
如果 |
|
如果 |
|
|
密钥库密码。如果设置了 |
|
|
如果 |
|
|
如果 |
|
|
如果 |
|
|
|
将此属性设置为 true 以在应用程序启动时解析远程密钥。 |