使用 Auth0 OpenID Connect 提供程序保护 Quarkus Web 应用程序
创建 Auth0 应用程序
前往 Auth0 控制面板并创建一个常规 Web 应用程序。例如,创建一个名为 QuarkusAuth0
的 Auth0 应用程序。

您的 Auth0 应用程序将创建,并附带客户端 ID、密钥和基于 HTTPS 的域名。请记下这些属性,因为您将在下一步中使用它们来完成 Quarkus 配置。

接下来,在 Auth0 控制面板中,向您的应用程序添加一些用户。

现在您已成功创建并配置了 Auth0 应用程序,可以开始创建和配置 Quarkus 端点。在接下来的步骤中,您还将继续配置和更新 Auth0 应用程序。
创建 Quarkus 应用程序
使用以下 Maven 命令创建一个 Quarkus REST (以前称为 RESTEasy Reactive) 应用程序,该应用程序可以使用 Quarkus OIDC 扩展进行保护。
对于 Windows 用户
-
如果使用 cmd,(不要使用反斜杠
\
并将所有内容放在同一行上) -
如果使用 Powershell,请将
-D
参数用双引号括起来,例如"-DprojectArtifactId=quarkus-auth0"
创建应用程序工作区并将其导入您喜欢的 IDE。让我们添加一个只能由已认证用户访问的 Jakarta REST 端点。
package org.acme;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
@IdToken (1)
JsonWebToken idToken;
@GET
@Authenticated (2)
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, " + idToken.getName();
}
}
1 | 注入的 JsonWebToken (JWT) bean 具有 @IdToken 限定符,这意味着它代表的是 OIDC ID 令牌 而不是访问令牌。IdToken 以声明的形式提供有关在 OIDC 授权码流中已认证的当前用户的信息,您可以使用 JsonWebToken API 来访问这些声明。 |
2 | io.quarkus.security.Authenticated 注解已添加到 hello() 方法,这意味着只有已认证用户才能访问它。 |
在授权码流期间获取的访问令牌,以及 ID 令牌,不会被端点直接使用,而仅用于代表当前已认证用户访问下游服务。在本教程稍后部分将详细介绍“访问令牌”。 |
通过使用您之前创建的 Auth0 应用程序的属性,在 Quarkus application.properties
文件中配置 OIDC。
# Make sure the application domain is prefixed with 'https://'
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=web-app
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
完成此步骤后,您已成功配置 Quarkus 使用您的 Auth0 应用程序的域名、客户端 ID 和密钥。设置属性 quarkus.oidc.application-type=web-app
可指示 Quarkus 使用 OIDC 授权码流,但也有其他方法,将在教程稍后讨论。
端点地址将是 https://:8080/hello,该地址也必须在您的 Auth0 应用程序中注册为允许的回调 URL。

完成此步骤后,当您从浏览器访问 Quarkus https://:8080/hello 端点时,身份验证完成后 Auth0 会将您重定向回同一地址。
默认情况下,Quarkus 会自动将当前请求路径用作回调路径。但您可以通过设置 Quarkus 在生产环境中,您的应用程序很可能拥有更大的 URL 空间,并提供多个端点地址。在这种情况下,您可以设置一个专用的回调 (重定向) 路径,并将此 URL 注册到提供程序的控制面板中,如下面的配置示例所示
在示例场景中,Quarkus 在接受来自 Auth0 的重定向、完成授权码流并创建会话 cookie 后,会调用 |
现在您可以开始测试端点了。
测试 Quarkus 端点
以开发模式启动 Quarkus
$ mvn quarkus:dev
这是本教程中唯一需要手动启动 Quarkus 开发模式的情况。本教程其余部分的配置和代码更新步骤将由 Quarkus 自动观察和处理,无需手动重新启动应用程序。 |
打开浏览器并访问 https://:8080/hello。
您将被重定向到 Auth0 并提示登录

并授权 QuarkusAuth0
应用程序访问您的帐户

最后,您将被重定向回 Quarkus 端点,该端点将返回以下响应:Hello, auth0|60e5a305e8da5a006aef5471
请注意,当前用户名未返回。要了解此行为发生的原因,您可以使用 OIDC Dev UI,如“Quarkus Dev Services 和 OIDC (OIDC)”指南的“所有 OIDC 提供程序的 Dev UI”部分和以下部分所述。 |
在 OIDC Dev UI 中查看 Auth0 令牌
Quarkus 提供了出色的 Dev UI 体验。特别是,Quarkus 为使用 Keycloak 容器开发和测试 OIDC 端点提供了内置支持。如果未为 Quarkus quarkus.oidc.auth-server-url
配置属性指定 OIDC 提供程序的地址,则 Keycloak 的 DevService 会自动启动并使用。
当提供程序已配置时,您可以继续使用 Quarkus OIDC Dev UI。请按照以下说明更新您的配置
首先,将您的 Quarkus 应用程序类型从 web-app
更改为 hybrid
,如下所示
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid (1)
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
1 | 应用程序类型已更改为 hybrid ,因为 OIDC Dev UI 目前仅支持 SPA (单页应用程序) 模式。OIDC Dev UI 单页应用程序使用其自己的 JavaScript,将用户认证到 OIDC 提供程序,并使用访问令牌作为 Bearer 令牌来访问 Quarkus 端点作为服务。 |
通常,Quarkus 必须配置为 quarkus.oidc.application-type=service
才能支持 Bearer
令牌身份验证,但它也支持 hybrid
应用程序类型,这意味着它可以同时支持授权码和 bearer 令牌流。
您还需要将 Auth0 应用程序配置为允许回调到 OIDC Dev UI。请使用以下 URL 格式
-
在此示例中,
${provider-name}
是auth0

现在您可以使用 OIDC Dev UI 和 Auth0 了。
在浏览器会话中打开 https://:8080/q/dev/。将显示一个 OpenId Connect 卡片,链接到 Auth0 提供程序 SPA,如下所示

点击 Auth0 provider,然后点击 Login into Single Page Application

您将被重定向到 Auth0 进行登录。然后您将被重定向到 OIDC Dev UI 控制面板,如下所示

在这里,您可以查看编码和解码格式的 ID 和访问令牌,将它们复制到剪贴板,或使用它们来测试服务。我们将在稍后测试端点,但现在先检查 ID 令牌

正如您所见,它没有表示用户名的声明,但如果您查看其 sub
(subject) 声明,您会发现其值与您直接从浏览器访问 Quarkus 端点时获得的值匹配,即 auth0|60e5a305e8da5a006aef5471
。
通过配置 Quarkus 在身份验证过程中请求标准的 OIDC profile
范围来解决此问题,这应该会导致 ID 令牌包含更多信息。
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
quarkus.oidc.authentication.scopes=profile (1)
1 | 除了默认的 openid 范围外,还请求 profile 范围。 |
返回到 https://:8080/q/dev/,重复登录 Auth0
的过程,然后再次检查 ID 令牌,现在您应该会看到 ID 令牌包含 name
声明。

当您直接从浏览器访问 Quarkus 端点时,您应该会获得用户名。清除浏览器 cookie 缓存,访问 https://:8080/hello,仍然会返回 Hello, auth0|60e5a305e8da5a006aef5471
。嗯,哪里出错了?
答案在于 org.eclipse.microprofile.jwt.JsonWebToken#getName()
实现的细节,根据 MicroProfile MP JWT RBAC 规范,它会检查 MP JWT 特定的 upn
声明,然后尝试 preferred_username
,最后是 sub
,这解释了为什么即使 ID 令牌包含 name
声明,您也会得到 Hello, auth0|60e5a305e8da5a006aef5471
的答案。我们可以通过更改端点 hello()
方法的实现来返回特定的声明值来轻松修复它。
package org.acme;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
@IdToken
JsonWebToken idToken;
@GET
@Authenticated
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, " + idToken.getClaim("name");
}
}
现在清除浏览器缓存,访问 https://:8080/hello,最终会返回用户名。
注销支持
现在您已通过 Auth0 帮助用户登录 Quarkus,您可能希望支持用户发起的注销。Quarkus 支持 RP 发起的和其他标准的 OIDC 注销机制,以及本地会话注销。
目前,Auth0 不支持标准的 OIDC RP 发起的注销,并且在其可发现的元数据中不提供结束会话端点 URL,但它提供了自己的注销机制,该机制的工作方式几乎与标准机制完全相同。
通过 Quarkus OIDC 可以轻松支持它。您必须配置一个 Auth0 结束会话端点 URL,并让 Quarkus 在 RP 发起的注销重定向请求到 Auth0 时包含 client-id
查询参数和注销后的 URL 作为 returnTo
查询参数。
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
quarkus.oidc.authentication.scopes=openid,profile
quarkus.oidc.end-session-path=v2/logout (1)
quarkus.oidc.logout.post-logout-uri-param=returnTo (2)
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id} (3)
quarkus.oidc.logout.path=/logout (4)
quarkus.oidc.logout.post-logout-path=/hello/post-logout (5)
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated (6)
1 | Auth0 不会在元数据中包含结束会话 URL,因此请通过手动配置 Auth0 结束会话端点 URL 来补充它。 |
2 | Auth0 不会识别标准的 post_logout_redirect_uri 查询参数,而是期望 returnTo 参数。 |
3 | Auth0 在注销请求中期望 client-id 。 |
4 | 对 /logout 路径的已认证请求将被视为 RP 发起的注销请求。 |
5 | 这是已注销用户应该返回的公共资源。 |
6 | 确保 /logout 路径受到保护。 |
在这里,我们自定义了 Auth0 结束会话端点 URL,并指示 Quarkus https://:8080/logout
请求必须触发当前已认证用户的注销。关于 /logout
路径的一个有趣之处在于它是 虚拟
的,它不被任何 JAX-RS 端点中的方法支持,因此为了让 Quarkus OIDC 能够响应 /logout
请求,我们直接在配置中为此路径附加了一个 authenticated
HTTP 安全策略。
我们还配置 Quarkus 将已注销的用户返回到公共 /hello/post-logout
资源,并且此路径作为 Auth0 特定的 returnTo
查询参数包含在注销请求中。最后,Quarkus 应用程序的 client-id
也包含在注销 URL 中。
更新端点以接受注销后的重定向
package org.acme;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.IdToken;
import io.quarkus.security.Authenticated;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
@IdToken
JsonWebToken idToken;
@GET
@Authenticated
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, " + idToken.getClaim("name");
}
@GET
@Path("post-logout")
@Produces(MediaType.TEXT_PLAIN)
public String postLogout() {
return "You were logged out";
}
}
请注意添加了公共 /hello/post-logout
资源方法。
在我们测试注销之前,请确保 Auth0
应用程序已配置为允许此注销重定向回 Quarkus,用户注销后。

现在,清除浏览器 cookie 缓存,访问 https://:8080/hello,使用 Auth0 登录 Quarkus,获取返回的用户名,然后转到 https://:8080/logout
。您将在浏览器中看到 You were logged out
消息。
接下来,转到 https://:8080/q/dev/,从 Dev UI SPA 登录 Auth0,并注意您现在也可以从 OIDC Dev UI 注销,在 Logged in as Sergey Beryozkin
文本旁边看到代表注销的符号。

为了让注销从 OIDC DevUI 工作,Auth0 应用程序的允许注销回调列表必须更新为包含 OIDC DevUI 端点。

现在直接从 OIDC Dev UI 注销,并以新用户身份登录 - 如果需要,请向已注册的 Auth0 应用程序添加更多用户。
基于角色的访问控制
我们已确认 Quarkus 端点可以通过 Auth0
进行身份验证的用户访问。
下一步是引入基于角色的访问控制 (RBAC),以便只有特定角色的用户,例如 admin
,才能访问端点。
另请参阅下面的 基于权限的访问控制 部分。
Auth0 令牌默认不包含任何包含角色的声明,因此,首先,您必须自定义 Auth0
应用程序的 Login
流,并使用自定义操作将角色添加到令牌。在 Auth0
控制面板中选择 Actions/Flows/Login
,选择 Add Action/Build Custom
,并将其命名为 AddRoleClaim
。

向其中添加以下操作脚本
exports.onExecutePostLogin = async (event, api) => {
const namespace = 'https://quarkus-security.com';
if (event.authorization) {
api.idToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
api.accessToken.setCustomClaim(`${namespace}/roles`, event.authorization.roles);
}
};
请注意,自定义 Auth0 声明必须进行命名空间限定,因此包含角色的声明将被命名为“https://quarkus-security.com/roles”。查看我们在前面部分分析过的 ID 令牌内容,您将看到此声明是如何表示的,例如
{
"https://quarkus-security.com/roles": [
"admin"
]
}
Auth0 登录流图现在应该如下所示

您必须将 admin
等角色添加到 Auth0
应用程序中已注册的用户。
创建一个 admin
角色

并将其添加到已注册用户

接下来,更新 Quarkus 端点,要求只有具有 admin
角色的用户才能访问端点。
package org.acme;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.IdToken;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
@IdToken
JsonWebToken idToken;
@GET
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, " + idToken.getClaim("name");
}
@GET
@Path("post-logout")
@Produces(MediaType.TEXT_PLAIN)
public String postLogout() {
return "You were logged out";
}
}
打开 https://:8080/hello,向 Auth0 进行身份验证并获得 403
。您获得 403
的原因是 Quarkus OIDC 不知道 Auth0
令牌中的哪个声明代表角色信息,默认情况下会检查 groups
声明,而 Auth0 令牌现在期望有一个“https://quarkus-security.com/roles”声明。
通过告知 Quarkus OIDC 必须检查哪个声明来强制执行 RBAC 来解决此问题
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles" (1)
# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated
1 | 指向自定义角色声明。由于声明已进行命名空间限定,因此角色声明的路径用双引号括起来。 |
现在,清除浏览器 cookie 缓存,再次访问 https://:8080/hello,向 Auth0 进行身份验证,并获得预期的用户名。
使用不透明的 Auth0 访问令牌访问 Quarkus
本节的主要目标是解释 Quarkus 如何调整为接受 有关如何配置 Auth0 和 Quarkus 以使授权码访问令牌以 JWT 格式颁发并传播到服务端点的更多信息,请参阅本教程的以下 传播访问令牌到微服务 和 JWT 格式的访问令牌 部分。 |
到目前为止,我们只测试了使用 OIDC 授权码流的 Quarkus 端点。在此流中,您使用浏览器访问 Quarkus 端点,Quarkus 本身管理授权码流,用户被重定向到 Auth0,登录,然后重定向回 Quarkus,Quarkus 通过交换代码以获取 ID、访问和刷新令牌来完成流,并使用代表成功用户身份验证的 ID 令牌。此时访问令牌无关紧要。如前所述,在授权码流中,Quarkus 仅使用访问令牌来代表当前已认证用户访问下游服务。
但让我们设想一下,我们开发的 Quarkus 端点也必须接受 Bearer
访问令牌:可能是其他 Quarkus 端点正在将其传播到此端点,或者可能是使用访问令牌访问 Quarkus 端点的 SPA。我们已经使用过的用于分析 ID 令牌的 Quarkus OIDC DevUI SPA 非常适合使用 SPA 可用的访问令牌来测试 Quarkus 端点。
让我们再次转到 https://:8080/q/dev-ui,选择 OpenId Connect
卡片,登录到 Auth0,并检查访问令牌内容。

此访问令牌与我们之前查看的 ID 令牌不同,Quarkus 无法直接验证它。这是因为访问令牌是 JWE
(加密) 格式,而不是 JWS
(签名) 格式。从解码的令牌头中可以看到,它已直接使用仅 Auth0 已知的密钥进行了加密,因此 Quarkus 无法解密其内容。从 Quarkus 的角度来看,此访问令牌是 不透明
的,Quarkus 无法使用公开的 Auth0 非对称验证密钥来验证它。
为了确认这一点,在 Test Service
区域将 /hello
输入为 Service Address
,然后按 With Access Token
,您将收到 HTTP 401
状态。

为了让 Quarkus 能够接受此类访问令牌,应提供以下两个选项之一。第一个选项是使用提供程序的内省端点远程内省不透明令牌。令牌内省通常在 OAuth2
级别支持,并且由于 OIDC
构建在 OAuth2
之上,因此一些 OIDC 提供程序 (如 Keycloak) 也支持令牌内省。但是,Auth0 不支持令牌内省,您可以通过查看公开的 Auth0 元数据来检查它,将 /.well-known/openid-configuration
添加到您已配置的 Auth0 提供程序的地址,然后打开生成的 URL https://dev-3ve0cgn7.us.auth0.com/.well-known/openid-configuration
。您将看到 Auth0 没有内省端点。

因此,可以使用第二个选项,即间接访问令牌验证,其中访问令牌用于从 Auth0 获取 UserInfo
,以接受和验证不透明的 Auth0 令牌。此选项有效,因为 OIDC 提供程序必须先验证访问令牌才能颁发 UserInfo
,而 Auth0 具有 UserInfo
端点。
因此,让我们配置 Quarkus,使其通过使用访问令牌来获取 UserInfo
来请求验证访问令牌。
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
# Point to the custom roles claim
quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles"
# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated
quarkus.oidc.token.verify-access-token-with-user-info=true (1)
1 | 通过使用访问令牌来获取 UserInfo 来间接验证访问令牌。 |
更新端点代码以期望 UserInfo
而不是 ID 令牌
。
package org.acme;
import io.quarkus.oidc.UserInfo;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/hello")
public class GreetingResource {
@Inject
UserInfo userInfo;
@GET
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, " + userInfo.getName();
}
@GET
@Path("post-logout")
@Produces(MediaType.TEXT_PLAIN)
public String postLogout() {
return "You were logged out";
}
}
此代码现在将同时适用于授权码和 bearer 访问令牌流。
让我们转到我们查看过访问令牌的 OIDC Dev UI,在 Test Service
区域将 /hello
输入为 Service Address
,然后按 With Access Token
,您将收到 200
。

为了确认它确实有效,请更新测试端点以仅允许 user
角色,使用 @RolesAllowed("user")
。再次尝试从 OIDC Dev UI 访问端点,您将收到 HTTP 403
错误。将代码恢复为 @RolesAllowed("admin")
以再次获得令人放心的 HTTP 200
状态。
当通过使用访问令牌来获取 UserInfo
来间接验证不透明访问令牌时,Quarkus 将使用 UserInfo
作为角色信息的来源 (如果存在)。碰巧的是,Auth0 也会在 UserInfo
响应中包含之前创建的自定义角色声明。
如本节开头所述,本节的主要目标是解释 Quarkus 如何验证不透明访问令牌。通常,应避免将仅用于检索 有关处理 Auth0 访问令牌的推荐方法,请参阅本教程的以下 传播访问令牌到微服务 和 JWT 格式的访问令牌 部分。 |
通常,我们使用访问令牌来访问远程服务,但 OIDC DevUI SPA 控制面板还提供了使用 ID 令牌进行测试的选项。此选项仅用于模拟 SPA 将端点委托以验证和检索 ID 令牌中的某些信息供 SPA 使用的情况 - 但 OIDC DevUI 仍会将 ID 令牌作为 Bearer 令牌发送到端点。在大多数情况下,优先使用访问令牌进行测试。 |
您可以从 OIDC DevUI 使用 SwaggerUI 或 GraphQL 来测试服务,而不是手动输入服务路径进行测试。例如,如果您在应用程序的 pom 中添加
您将在 OIDC Dev UI 中看到 Swagger 链接。 ![]() 单击 Swagger 链接并开始测试服务。 |
传播访问令牌到微服务
既然我们已经成功使用了 OIDC 授权码流,并使用了 ID 令牌和 UserInfo 来访问用户信息,那么下一个典型的任务是将当前的 Auth0 访问令牌传播到下游服务,以代表当前已认证用户进行访问。
实际上,最后一个代码示例,显示注入的 UserInfo
,是访问令牌传播的一个具体示例,在这种情况下,Quarkus 将 Auth0 访问令牌传播到 Auth0 UserInfo
端点以获取 UserInfo
。Quarkus 在用户无需执行任何操作的情况下即可完成此操作。
但是,对于将访问令牌传播到某些自定义服务呢?在 Quarkus 中,这很容易实现,对于授权码和 bearer 令牌流都是如此。您需要做的就是创建一个 REST Client 接口,用于调用需要 Bearer 令牌访问的服务,并使用 @AccessToken
进行注解,然后到达前端端点的访问令牌,无论是 Auth0 Bearer 访问令牌还是 Quarkus 在完成 Auth0 授权码流后获得的令牌,都将被传播到目标微服务。这非常简单。
有关传播访问令牌的示例,请参阅本教程的以下各节。有关令牌传播的更多信息,请参阅 OIDC 令牌传播。
JWT 格式的访问令牌
我们已经详细介绍了 Quarkus OIDC 如何处理 使用不透明的 Auth0 访问令牌访问 Quarkus,但我们不希望将 Auth0 不透明令牌传播到为当前已认证用户执行有意义操作的微服务,除了检查其 UserInfo。
前端 Quarkus 应用程序将通过传播授权码流访问令牌来访问的微服务在 Auth0 控制面板中表示为 API
。让我们在 Applications/APIs
中添加它。

创建的 QuarkusAuth0API
的 https://quarkus-auth0
标识符将作为此 API 的 audience
。在授权码流重定向到 Auth0 时,将此 audience 作为查询参数提供,将确保 Auth0 以 JWT 格式颁发访问令牌。
API 微服务
向项目添加以下依赖项以支持 OIDC 令牌传播和 REST Client。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-jackson</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-oidc-token-propagation</artifactId>
</dependency>
创建 ApiEchoService
服务类
package org.acme;
import io.quarkus.security.Authenticated;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/echo")
public class ApiEchoService {
@POST
@Authenticated
@Produces(MediaType.TEXT_PLAIN)
public String echoUserName(String username) {
return username;
}
}
并将其配置为 OIDC service
应用程序,该应用程序将仅从 Auth0 获取公共验证密钥。此微服务的配置应只有一行。
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
这对于 OIDC service
应用程序获取 Auth0 公共验证密钥并使用它们来验证 JWT 格式的 Auth0 访问令牌就足够了。
在本教程中,您已经配置了 OIDC
|
接下来,添加一个表示 ApiEchoService
的 REST Client 接口。
package org.acme;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
@RegisterRestClient
@AccessToken (1)
@Path("/echo")
public interface ApiEchoServiceClient {
@POST
@Produces(MediaType.TEXT_PLAIN)
String echoUserName(String username);
}
1 | 将访问令牌作为 HTTP Authorization: Bearer accesstoken 头进行传播 |
并更新 Quarkus 前端应用程序 GreetingResource
的配置,该应用程序已在此之前创建,以请求授权码流访问令牌 (而非 ID 令牌) 包含针对 ApiEchoService
的 aud
(audience) 声明,并配置 ApiEchoService
REST Client 的基本 URL。
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.authentication.extra-params.audience=https://quarkus-auth0 (1)
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
# Point to the custom roles claim
quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles"
# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated
quarkus.oidc.token.verify-access-token-with-user-info=true
org.acme.ApiEchoServiceClient/mp-rest/url=https://:${port} (2)
quarkus.test.native-image-profile=test
%prod.port=8080
%dev.port=8080
%test.port=8081
1 | 在从 Quarkus 到 Auth0 的授权码流重定向过程中,将额外的 audience 查询参数传递给 Auth0 授权端点。这将确保颁发的访问令牌是 JWT 格式,并包含一个 aud (audience) 声明,该声明将包含 https://quarkus-auth0 。 |
2 | 将 ApiEchoServiceClient 指向 ApiEchoService 端点。org.acme.ApiEchoServiceClient/mp-rest/url=https://:${port} 属性中的 HTTP 端口是参数化的,以确保在开发、测试和生产模式下构建正确的 URL。 |
最后,更新 GreetingResource
以请求 ApiEchoService
回显用户名。
package org.acme;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import org.eclipse.microprofile.rest.client.inject.RestClient;
@Path("/hello")
public class GreetingResource {
@Inject
@RestClient
ApiEchoServiceClient echoClient; (1)
@Inject
UserInfo userInfo;
@GET
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String hello() {
return "Hello, " + echoClient.echoUserName(userInfo.getName()); (2)
}
@GET
@Path("post-logout")
@Produces(MediaType.TEXT_PLAIN)
public String postLogout() {
return "You were logged out";
}
}
1 | 注入 ApiEchoServiceClient REST Client |
2 | 使用 ApiEchoServiceClient 回显用户名。 |
打开浏览器,访问 https://:8080/hello,然后在浏览器中显示您的姓名。
转到 https://:8080/q/dev-ui,选择 OpenId Connect
卡片,登录到 Auth0,并检查访问令牌内容。

正如您所见,访问令牌不再像 使用不透明的 Auth0 访问令牌访问 Quarkus 部分所示那样加密,实际上它现在是 JWT 格式的。
基于权限的访问控制
我们在 基于角色的访问控制 部分讨论了如何让 Quarkus 检查包含用户角色的命名空间限定声明,并使用此信息来强制执行基于角色的访问控制。您已将 Auth0 配置为将自定义角色声明添加到 ID 和访问令牌。
然而,基于权限的访问控制更适合这种情况:前端端点将访问令牌传播到一个微服务,该微服务将检查给定的访问令牌是否已被授权为该服务执行具体操作,而不是此令牌证明用户属于某个特定角色。例如,管理员角色并不一定意味着用户可以对该微服务的某些内容具有读写访问权限。
让我们看看如何将基于权限的访问控制约束应用于 ApiEchoService
。
转到 Auth0 控制面板,向 QuarkusAuth0API
API 添加 echo:name
权限。

如果在此范围也要求在授权码流期间,echo:name
权限将作为标准的 OAuth2 scope
声明值包含在访问令牌中。更新配置如下。
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile,echo:name (1)
quarkus.oidc.authentication.extra-params.audience=https://quarkus-auth0
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
# Point to the custom roles claim
quarkus.oidc.roles.role-claim-path="https://quarkus-security.com/roles"
# Logout
quarkus.oidc.end-session-path=v2/logout
quarkus.oidc.logout.post-logout-uri-param=returnTo
quarkus.oidc.logout.extra-params.client_id=${quarkus.oidc.client-id}
quarkus.oidc.logout.path=/logout
quarkus.oidc.logout.post-logout-path=/hello/post-logout
quarkus.http.auth.permission.authenticated.paths=/logout
quarkus.http.auth.permission.authenticated.policy=authenticated
quarkus.oidc.token.verify-access-token-with-user-info=true
org.acme.ApiEchoServiceClient/mp-rest/url=https://:8080
1 | 在授权码流期间将请求额外的 echo:name 范围。 |
现在更新 ApiEchoService
以强制执行基于权限的访问控制。
package org.acme;
import io.quarkus.security.PermissionsAllowed;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/echo")
public class ApiEchoService {
@POST
@PermissionsAllowed("echo:name")
@Produces(MediaType.TEXT_PLAIN)
String echoUserName(String username) {
return username;
}
}
这一切都是必需的,因为 Quarkus OIDC 会自动将 scope
声明值作为权限与当前安全身份相关联。
您可以通过组合 |
打开浏览器,访问 https://:8080/hello,然后在浏览器中显示名称。
为了确认权限已正确强制执行,请将其更改为 echo.name
:@PermissionsAllowed("echo.name")
。清除浏览器缓存,再次访问 https://:8080/hello,ApiEchoService
将报告 403
。现在将其恢复为 @PermissionsAllowed("echo:name")
。
集成测试
您已经使用 OIDC DevUI SPA 登录到 Auth0,并使用访问令牌测试了 Quarkus 端点,在此过程中更新了端点代码。
但是,运行测试也很重要,让我们看看如何使用 Quarkus Continuous Testing 功能来测试在本教程过程中开发的端点和配置。
从以下测试代码开始。
package org.acme;
import io.quarkus.test.junit.QuarkusTest;
import org.junit.jupiter.api.Test;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
@QuarkusTest
public class GreetingResourceTest {
@Test
public void testHelloEndpoint() {
given()
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Sergey Beryozkin"));
}
}
如果您还记得,当应用程序以开发模式启动时,您可以在 CLI 窗口中看到以下内容。

按 r
,注意此测试因 403
而失败,这是预期的,因为测试没有向端点发送令牌。

在修复测试之前,让我们回顾一下用于测试 Quarkus 端点的选项,这些端点受 OIDC 保护。这些选项可能因您的应用程序支持的流以及您偏好的测试方式而异。使用 OIDC 授权码流的端点可以使用 这些选项之一 进行测试,而使用 Bearer 令牌身份验证的端点可以使用 这些选项之一 进行测试。
如您所见,可以使用 Wiremock
或 @TestSecurity
注解来测试受 Auth0 保护的端点。尝试自己编写此类测试,如果您遇到任何问题,请联系我们。
在本教程中,我们将使用最近添加的 OidcTestClient
来支持测试使用实时 Auth0 开发租户的端点。
这是相关的配置片段。
quarkus.oidc.auth-server-url=https://dev-3ve0cgn7.us.auth0.com
quarkus.oidc.application-type=hybrid
quarkus.oidc.authentication.scopes=profile
quarkus.oidc.client-id=sKQu1dXjHB6r0sra0Y1YCqBZKWXqCkly
quarkus.oidc.credentials.secret=${client-secret}
在生产环境中,您将使用 %prod.
和 %test.
限定符来区分生产和测试级别的配置。假设上述配置在您的实际应用程序中确实带有 %test.
前缀,并且此配置还包括 %prod.
限定的 Auth0 生产租户配置。
使用 OidcTestClient
测试此类配置需要使用 OAuth2 password
或 client_credentials
授予来从 Auth0 开发租户获取令牌,我们将尝试 password
授予。确保 Auth0 控制面板中注册的应用程序允许 password
授予。

重要的是要明确,我们不建议在生产环境中使用已弃用的 OAuth2 |
OidcTestClient
应用于测试接受 bearer 令牌的应用程序,这将适用于本教程中开发的端点,因为它同时支持授权码流和 bearer 令牌身份验证。如果您只想支持授权码流,则需要直接使用 OIDC WireMock 或 HtmlUnit
来处理 Auth0 开发租户,在后一种情况下,HtmlUnit
测试代码必须与 Auth0 如何提示用户输入凭据保持一致。如果您愿意,可以从文档中复制 HtmlUnit 测试片段 并进行实验。
同时,我们将继续修复当前失败的测试,方法是使用 OidcTestClient
。
首先,您必须添加以下依赖项。
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-oidc-server")
它提供了一个实用类 io.quarkus.test.oidc.client.OidcTestClient
,可以在测试中用于获取访问令牌 (此依赖项还提供 OIDC WireMock 支持 - 如果您想,请查看文档了解如何使用它进行测试)。
现在像这样更新测试代码。
package org.acme;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import java.util.Map;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Test;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.client.OidcTestClient;
@QuarkusTest
public class GreetingResourceTest {
static OidcTestClient oidcTestClient = new OidcTestClient();
@AfterAll
public static void close() {
client.close();
}
@Test
public void testHelloEndpoint() {
given()
.auth().oauth2(getAccessToken(`sberyozkin@gmail.com`, "userpassword"))
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Sergey Beryozkin"));
}
private String getAccessToken(String name, String secret) {
return oidcTestClient.getAccessToken(name, secret, (1)
Map.of("audience", "https://quarkus-auth0",
"scope", "openid profile"));
}
}
1 | OidcTestClient 用于获取访问令牌,使用已注册用户的用户名和密码,以及 audience 和 scope 参数。 |
OidcTestClient
将自行查找 Auth0
令牌端点地址、客户端 ID 和密钥。
再次按 r
,让测试通过。

顺便说一句,如果您愿意,可以直接从 DevUI 在连续模式下运行测试。

生产模式
您已经在开发模式下开发和测试了受 Auth0 保护的 Quarkus 端点。下一步是在生产模式下运行您的应用程序。在 JVM 和本机模式之间进行选择。
以 JVM 模式运行应用程序
编译应用程序
quarkus build
./mvnw install
./gradlew build
运行应用程序
java -jar target/quarkus-app/quarkus-run.jar
打开浏览器,访问 https://:8080/hello,然后在浏览器中显示名称。
在原生模式下运行应用程序
您无需进行任何修改即可将相同的演示编译为本机模式。这意味着您不再需要在生产环境中安装 JVM。运行时技术包含在生成的二进制文件中,并针对以最少资源运行进行了优化。
编译需要一段时间,因此默认情况下禁用此步骤。
通过启用 native
配置文件再次构建您的应用程序
quarkus build --native
./mvnw install -Dnative
./gradlew build -Dquarkus.native.enabled=true
接下来直接运行以下二进制文件。
./target/quarkus-auth0-1.0.0-SNAPSHOT-runner
打开浏览器,访问 https://:8080/hello,然后在浏览器中显示名称。
故障排除
本教程中描述的步骤应与教程描述的完全相同。您可能需要清除浏览器 cookie 来访问已更新的 Quarkus 端点 (如果您已完成身份验证)。您可能需要在开发模式下手动重新启动 Quarkus 应用程序,但这并非预期情况。如果您在完成本教程时需要帮助,可以与 Quarkus 团队联系。
总结
本教程演示了如何使用 quarkus-oidc
扩展和 Auth0,通过授权码和 Bearer 令牌身份验证流来保护 Quarkus 端点,这两种流都由相同的端点代码支持。无需编写任何代码,您就添加了对自定义 Auth0 注销流的支持,并使用自定义 Auth0 命名空间限定声明启用了基于角色的访问控制。通过将 @AccessToken
注解添加到微服务 REST Client,实现了从前端端点到微服务端的令牌传播。微服务 endpoint 使用 @PermissionsAllowed
注解激活了基于权限的访问控制。您使用 Quarkus 开发模式在不重新启动端点的情况下更新代码和配置,并且还使用 OIDC Dev UI 来可视化和测试 Auth0 令牌。您使用 Quarkus 的连续测试功能,通过与实时 Auth0 开发租户的集成测试来补充 OIDC Dev UI 测试。最后,您在 JVM 和本机模式下运行了应用程序。
享受!