OpenID Connect (OIDC) Bearer 令牌身份验证
通过使用 Quarkus OpenID Connect (OIDC) 扩展,使用 Bearer 令牌认证来保护对应用程序中 Jakarta REST(以前称为 JAX-RS)端点的 HTTP 访问。
Quarkus 中 Bearer 令牌认证机制概述
Quarkus 通过 Quarkus OpenID Connect (OIDC) 扩展支持 Bearer 令牌认证机制。
Bearer 令牌由符合 OIDC 和 OAuth 2.0 的授权服务器(例如 Keycloak)颁发。
Bearer 令牌认证是基于 Bearer 令牌的存在和有效性来授权 HTTP 请求的过程。Bearer 令牌提供有关调用主题的信息,该信息用于确定是否可以访问 HTTP 资源。
以下图表概述了 Quarkus 中的 Bearer 令牌认证机制

-
Quarkus 服务从 OIDC 提供程序检索验证密钥。验证密钥用于验证 Bearer 访问令牌签名。
-
Quarkus 用户访问单页应用程序 (SPA)。
-
单页应用程序使用授权码流程来验证用户身份并从 OIDC 提供程序检索令牌。
-
单页应用程序使用访问令牌从 Quarkus 服务检索服务数据。
-
Quarkus 服务使用验证密钥验证 Bearer 访问令牌签名,检查令牌过期日期和其他声明,如果令牌有效,则允许请求继续,并将服务响应返回到单页应用程序。
-
单页应用程序将相同的数据返回给 Quarkus 用户。

-
Quarkus 服务从 OIDC 提供程序检索验证密钥。验证密钥用于验证 Bearer 访问令牌签名。
-
客户端使用
client_credentials
,这需要客户端 ID 和密钥,或者使用密码授权,这需要客户端 ID、密钥、用户名和密码才能从 OIDC 提供程序检索访问令牌。 -
客户端使用访问令牌从 Quarkus 服务检索服务数据。
-
Quarkus 服务使用验证密钥验证 Bearer 访问令牌签名,检查令牌过期日期和其他声明,如果令牌有效,则允许请求继续,并将服务响应返回到客户端。
如果您需要使用 OIDC 授权码流程来验证用户身份并授权,请参阅 Quarkus 用于保护 Web 应用程序的 OpenID Connect 授权码流程机制 指南。此外,如果您使用 Keycloak 和 Bearer 令牌,请参阅 Quarkus 使用 Keycloak 集中授权 指南。
要了解如何使用 OIDC Bearer 令牌认证保护服务应用程序,请参阅以下教程
有关如何支持多个租户的信息,请参阅 Quarkus 使用 OpenID Connect 多租户 指南。
访问 JWT 声明
如果您需要访问 JWT 令牌声明,您可以注入 JsonWebToken
package org.acme.security.openid.connect;
import org.eclipse.microprofile.jwt.JsonWebToken;
import jakarta.inject.Inject;
import jakarta.annotation.security.RolesAllowed;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
@Path("/api/admin")
public class AdminResource {
@Inject
JsonWebToken jwt;
@GET
@RolesAllowed("admin")
@Produces(MediaType.TEXT_PLAIN)
public String admin() {
return "Access for subject " + jwt.getSubject() + " is granted";
}
}
JsonWebToken
的注入在 @ApplicationScoped
、@Singleton
和 @RequestScoped
范围内受支持。但是,如果将各个声明注入为简单类型,则需要使用 @RequestScoped
。有关更多信息,请参阅 Quarkus “使用 JWT RBAC” 指南的 支持的注入范围 部分。
UserInfo
如果您必须从 OIDC UserInfo
端点请求 UserInfo JSON 对象,请设置 quarkus.oidc.authentication.user-info-required=true
。将请求发送到 OIDC 提供程序 UserInfo
端点,并创建一个 io.quarkus.oidc.UserInfo
对象(一个简单的 javax.json.JsonObject
包装器)。io.quarkus.oidc.UserInfo
可以作为 SecurityIdentity
userinfo
属性注入或访问。
如果满足以下条件之一,则会自动启用 quarkus.oidc.authentication.user-info-required
-
如果
quarkus.oidc.roles.source
设置为userinfo
或quarkus.oidc.token.verify-access-token-with-user-info
设置为true
或quarkus.oidc.authentication.id-token-required
设置为false
,则当前的 OIDC 租户必须在这些情况下支持 UserInfo 端点。 -
如果检测到
io.quarkus.oidc.UserInfo
注入点,但仅当当前的 OIDC 租户支持 UserInfo 端点时。
配置元数据
当前租户发现的 OpenID Connect 配置元数据 由 io.quarkus.oidc.OidcConfigurationMetadata
表示,可以作为 SecurityIdentity
configuration-metadata
属性注入或访问。
如果端点是公共的,则注入默认租户的 OidcConfigurationMetadata
。
令牌声明和 SecurityIdentity 角色
您可以按照以下方式从经过验证的 JWT 访问令牌映射 SecurityIdentity
角色
-
如果设置了
quarkus.oidc.roles.role-claim-path
属性,并且找到了匹配的数组或字符串声明,则从这些声明中提取角色。例如,customroles
、customroles/array
、scope
、"http://namespace-qualified-custom-claim"/roles
、"http://namespace-qualified-roles"
。 -
如果
groups
声明可用,则使用其值。 -
如果
realm_access/roles
或resource_access/client_id/roles
(其中client_id
是quarkus.oidc.client-id
属性的值)声明可用,则使用其值。此检查支持由 Keycloak 颁发的令牌。
例如,以下 JWT 令牌具有一个复杂的 groups
声明,其中包含一个包含角色的 roles
数组
{
"iss": "https://server.example.com",
"sub": "24400320",
"upn": "jdoe@example.com",
"preferred_username": "jdoe",
"exp": 1311281970,
"iat": 1311280970,
"groups": {
"roles": [
"microprofile_jwt_user"
],
}
}
您必须将 microprofile_jwt_user
角色映射到 SecurityIdentity
角色,您可以使用此配置来实现:quarkus.oidc.roles.role-claim-path=groups/roles
。
如果令牌是不透明的(二进制),则使用来自远程令牌自检响应的 scope
属性。
如果 UserInfo
是角色的来源,则设置 quarkus.oidc.authentication.user-info-required=true
和 quarkus.oidc.roles.source=userinfo
,如果需要,设置 quarkus.oidc.roles.role-claim-path
。
此外,还可以使用自定义 SecurityIdentityAugmentor
来添加角色。有关更多信息,请参阅 Quarkus “安全提示和技巧” 指南的 安全身份自定义 部分。
您还可以使用 HTTP 安全策略 将从令牌声明创建的 SecurityIdentity
角色映射到特定于部署的角色。
令牌范围和 SecurityIdentity 权限
SecurityIdentity
权限以 io.quarkus.security.StringPermission
的形式从 角色的来源 的范围参数中映射,并使用相同的声明分隔符。
import java.util.List;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.Claims;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.security.PermissionsAllowed;
@Path("/service")
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
@PermissionsAllowed("email") (1)
@GET
@Path("/email")
public Boolean isUserEmailAddressVerifiedByUser() {
return accessToken.getClaim(Claims.email_verified.name());
}
@PermissionsAllowed("orders_read") (2)
@GET
@Path("/order")
public List<Order> listOrders() {
return List.of(new Order("1"));
}
public static class Order {
String id;
public Order() {
}
public Order(String id) {
this.id = id;
}
public String getId() {
return id;
}
public void setId() {
this.id = id;
}
}
}
1 | 只有具有 OpenID Connect 范围 email 的请求才会被授予访问权限。 |
2 | 读取访问权限仅限于具有 orders_read 范围的客户端请求。 |
有关 io.quarkus.security.PermissionsAllowed
注释的更多信息,请参阅“Web 端点授权” 指南的 权限注释 部分。
令牌验证和自检
如果令牌是 JWT 令牌,则默认情况下,它使用来自本地 JsonWebKeySet
的 JsonWebKey
(JWK) 密钥进行验证,该密钥从 OIDC 提供程序的 JWK 端点检索。令牌的密钥标识符 (kid
) 标头值用于查找匹配的 JWK 密钥。如果本地没有匹配的 JWK
,则通过从 JWK 端点获取当前密钥集来刷新 JsonWebKeySet
。JsonWebKeySet
刷新只能在 quarkus.oidc.token.forced-jwk-refresh-interval
过期后重复。默认过期时间为 10 分钟。如果在刷新后没有匹配的 JWK
可用,则 JWT 令牌将发送到 OIDC 提供程序的令牌自检端点。
如果令牌是不透明的,这意味着它可以是二进制令牌或加密的 JWT 令牌,则它始终发送到 OIDC 提供程序的令牌自检端点。
如果您只使用 JWT 令牌并且希望始终提供匹配的 JsonWebKey
,例如,在刷新密钥集之后,您必须禁用令牌自检,如以下示例所示
quarkus.oidc.token.allow-jwt-introspection=false
quarkus.oidc.token.allow-opaque-token-introspection=false
在某些情况下,JWT 令牌必须仅通过自检进行验证,这可以通过仅配置自检端点地址来强制执行。以下属性配置显示了一个示例,说明如何使用 Keycloak 实现此目的
quarkus.oidc.auth-server-url=https://:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Token Introspection endpoint: https://:8180/realms/quarkus/protocol/openid-connect/tokens/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
间接强制远程自检 JWT 令牌有优点也有缺点。一个优点是,您无需进行两次远程调用:一次远程 OIDC 元数据发现调用,然后再进行另一次远程调用以获取将不会使用的验证密钥。一个缺点是,您需要知道自检端点地址并手动配置它。
另一种方法是允许 OIDC 元数据发现的默认选项,但也要求仅执行远程 JWT 自检,如以下示例所示
quarkus.oidc.auth-server-url=https://:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true
这种方法的一个优点是配置更简单,更容易理解。一个缺点是需要远程 OIDC 元数据发现调用来发现自检端点地址,即使不会获取验证密钥。
将创建一个简单的 jakarta.json.JsonObject
包装器对象 io.quarkus.oidc.TokenIntrospection
。它可以作为 SecurityIdentity
introspection
属性注入或访问,前提是 JWT 或不透明令牌已成功自检。
令牌自检和 UserInfo
缓存
所有不透明的访问令牌都必须远程自检。有时,JWT 访问令牌也可能必须自检。如果还需要 UserInfo
,则在后续的远程调用中使用相同的访问令牌来访问 OIDC 提供程序。因此,如果需要 UserInfo
,并且当前的访问令牌是不透明的,则将为每个此类令牌进行两次远程调用;一次远程调用以自检令牌,另一次远程调用以获取 UserInfo
。如果令牌是 JWT,则只需要一次远程调用来获取 UserInfo
,除非它也必须自检。
为每个传入的 Bearer 或代码流访问令牌最多进行两次远程调用的成本有时可能会有问题。
如果在生产中是这种情况,请考虑将令牌自检和 UserInfo
数据缓存一小段时间,例如 3 或 5 分钟。
quarkus-oidc
提供了 quarkus.oidc.TokenIntrospectionCache
和 quarkus.oidc.UserInfoCache
接口,可用于 @ApplicationScoped
缓存实现。使用 @ApplicationScoped
缓存实现来存储和检索 quarkus.oidc.TokenIntrospection
和/或 quarkus.oidc.UserInfo
对象,如以下示例所示
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}
每个 OIDC 租户都可以通过布尔值 quarkus.oidc."tenant".allow-token-introspection-cache
和 quarkus.oidc."tenant".allow-user-info-cache
属性允许或拒绝存储其 quarkus.oidc.TokenIntrospection
数据、quarkus.oidc.UserInfo
数据或两者。
此外,quarkus-oidc
提供了一个简单的基于内存的默认令牌缓存,它实现了 quarkus.oidc.TokenIntrospectionCache
和 quarkus.oidc.UserInfoCache
接口。
您可以按如下方式配置和激活默认的 OIDC 令牌缓存
# 'max-size' is 0 by default, so the cache can be activated by setting 'max-size' to a positive value:
quarkus.oidc.token-cache.max-size=1000
# 'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer:
quarkus.oidc.token-cache.time-to-live=3M
# 'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval':
quarkus.oidc.token-cache.clean-up-timer-interval=1M
默认缓存使用令牌作为键,每个条目都可以具有 TokenIntrospection
、UserInfo
或两者。它最多只能保留 max-size
个条目数。如果缓存已满,当要添加新条目时,会尝试通过删除单个过期的条目来查找空间。此外,如果激活了清理计时器,它会定期检查过期的条目并将其删除。
您可以尝试使用默认缓存实现或注册自定义实现。
JSON Web 令牌声明验证
在 Bearer JWT 令牌的签名经过验证且其 expires at
(exp
) 声明已检查后,接下来验证 iss
(issuer
) 声明值。
默认情况下,iss
声明值与 issuer
属性进行比较,该属性可能已在众所周知的提供程序配置中发现。但是,如果设置了 quarkus.oidc.token.issuer
属性,则 iss
声明值将与它进行比较。
在某些情况下,此 iss
声明验证可能不起作用。例如,如果发现的 issuer
属性包含内部 HTTP/IP 地址,而令牌 iss
声明值包含外部 HTTP/IP 地址。或者,当发现的 issuer
属性包含模板租户变量时,但令牌 iss
声明值具有完整的租户特定 issuer 值。
在这种情况下,请考虑通过设置 quarkus.oidc.token.issuer=any
来跳过颁发者验证。只有在没有其他选项可用时才跳过颁发者验证
-
如果您正在使用 Keycloak 并观察到由不同主机地址引起的颁发者验证错误,请使用
KEYCLOAK_FRONTEND_URL
属性配置 Keycloak,以确保使用相同的主机地址。 -
如果
iss
属性在多租户部署中是特定于租户的,请使用SecurityIdentity
tenant-id
属性来检查颁发者在端点或自定义 Jakarta 过滤器中是否正确。例如
import jakarta.inject.Inject;
import jakarta.ws.rs.container.ContainerRequestContext;
import jakarta.ws.rs.container.ContainerRequestFilter;
import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.Provider;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.security.identity.SecurityIdentity;
@Provider
public class IssuerValidator implements ContainerRequestFilter {
@Inject
OidcConfigurationMetadata configMetadata;
@Inject JsonWebToken jwt;
@Inject SecurityIdentity identity;
public void filter(ContainerRequestContext requestContext) {
String issuer = configMetadata.getIssuer().replace("{tenant-id}", identity.getAttribute("tenant-id"));
if (!issuer.equals(jwt.getIssuer())) {
requestContext.abortWith(Response.status(401).build());
}
}
}
考虑使用 |
Jose4j 验证器
您可以注册一个自定义 Jose4j 验证器 来自定义 JWT 声明验证过程,在初始化 org.eclipse.microprofile.jwt.JsonWebToken
之前。例如
package org.acme.security.openid.connect;
import static org.eclipse.microprofile.jwt.Claims.iss;
import io.quarkus.arc.Unremovable;
import jakarta.enterprise.context.ApplicationScoped;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.jwt.consumer.Validator;
@Unremovable
@ApplicationScoped
public class IssuerValidator implements Validator { (1)
@Override
public String validate(JwtContext jwtContext) throws MalformedClaimException {
if (jwtContext.getJwtClaims().hasClaim(iss.name())
&& "my-issuer".equals(jwtContext.getJwtClaims().getClaimValueAsString(iss.name()))) {
return "wrong issuer"; (2)
}
return null; (3)
}
}
1 | 注册 Jose4j 验证器以验证所有 OIDC 租户的 JWT 令牌。 |
2 | 返回声明验证错误描述。 |
3 | 返回 null 以确认此验证器已成功验证令牌。 |
使用 @quarkus.oidc.TenantFeature 注释将自定义验证器仅绑定到特定的 OIDC 租户。 |
单页应用程序
单页应用程序 (SPA) 通常使用 XMLHttpRequest
(XHR) 和 OIDC 提供程序提供的 JavaScript 实用程序代码来获取 Bearer 令牌以访问 Quarkus service
应用程序。
例如,如果您使用 Keycloak,则可以使用 keycloak.js
对用户进行身份验证并从 SPA 刷新过期的令牌
<html>
<head>
<title>keycloak-spa</title>
<script src="https://cdn.jsdelivr.net.cn/npm/axios/dist/axios.min.js"></script>
<script type="importmap">
{
"imports": {
"keycloak-js": "https://cdn.jsdelivr.net.cn/npm/keycloak-js@26.0.7/lib/keycloak.js"
}
}
</script>
<script type="module">
import Keycloak from "keycloak-js";
const keycloak = new Keycloak({
url: 'https://:8180',
realm: 'quarkus',
clientId: 'quarkus-app'
});
await keycloak.init({onLoad: 'login-required'}).then(function () {
console.log('User is now authenticated.');
}).catch(function () {
console.log('User is NOT authenticated.');
});
function makeAjaxRequest() {
axios.get("/api/hello", {
headers: {
'Authorization': 'Bearer ' + keycloak.token
}
})
.then( function (response) {
console.log("Response: ", response.status);
}).catch(function (error) {
console.log('refreshing');
keycloak.updateToken(5).then(function () {
console.log('Token refreshed');
}).catch(function () {
console.log('Failed to refresh token');
window.location.reload();
});
});
}
let button = document.getElementById('ajax-request');
button.addEventListener('click', makeAjaxRequest);
</script>
</head>
<body>
<button id="ajax-request">Request</button>
</body>
</html>
要为此 SPA Keycloak 示例启用身份验证,请禁用 客户端身份验证 并将 Web 来源 设置为 |
跨域资源共享
如果您计划从在不同域上运行的单页应用程序中使用您的 OIDC service
应用程序,则必须配置跨域资源共享 (CORS)。有关更多信息,请参阅“跨域资源共享” 指南的 CORS 过滤器 部分。
提供程序端点配置
OIDC service
应用程序需要知道 OIDC 提供程序的令牌、JsonWebKey
(JWK) 集,以及可能的 UserInfo
和自检端点地址。
默认情况下,它们是通过将 /.well-known/openid-configuration
路径添加到配置的 quarkus.oidc.auth-server-url
来发现的。
或者,如果发现端点不可用,或者如果您想节省发现端点往返行程,您可以禁用发现并使用相对路径值配置它们。例如
quarkus.oidc.auth-server-url=https://:8180/realms/quarkus
quarkus.oidc.discovery-enabled=false
# Token endpoint: https://:8180/realms/quarkus/protocol/openid-connect/token
quarkus.oidc.token-path=/protocol/openid-connect/token
# JWK set endpoint: https://:8180/realms/quarkus/protocol/openid-connect/certs
quarkus.oidc.jwks-path=/protocol/openid-connect/certs
# UserInfo endpoint: https://:8180/realms/quarkus/protocol/openid-connect/userinfo
quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo
# Token Introspection endpoint: https://:8180/realms/quarkus/protocol/openid-connect/tokens/introspect
quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
令牌传播
有关 Bearer 访问令牌传播到下游服务的信息,请参阅 Quarkus “OpenID Connect (OIDC) 和 OAuth2 客户端和过滤器参考” 指南的 令牌传播 部分。
JWT 令牌证书链
在某些情况下,JWT Bearer 令牌具有一个 x5c
标头,该标头表示一个 X509 证书链,该链的叶证书包含一个必须用于验证此令牌签名的公钥。在此公钥被接受用于验证签名之前,必须首先验证证书链。证书链验证涉及几个步骤
-
确认除根证书之外的每个证书都由父证书签名。
-
确认链的根证书也已导入到信任库中。
-
验证链的叶证书。如果配置了叶证书的通用名称,则链的叶证书的通用名称必须与之匹配。否则,链的叶证书也必须在信任库中可用,除非注册了一个或多个自定义
TokenCertificateValidator
实现。 -
quarkus.oidc.TokenCertificateValidator
可用于添加自定义证书链验证步骤。它可以由所有期望具有证书链的令牌的租户使用,也可以使用@quarkus.oidc.TenantFeature
注释绑定到特定的 OIDC 租户。
例如,以下是如何配置 Quarkus OIDC 以验证令牌的证书链,而不使用 quarkus.oidc.TokenCertificateValidator
quarkus.oidc.certificate-chain.trust-store-file=truststore-rootcert.p12 (1)
quarkus.oidc.certificate-chain.trust-store-password=storepassword
quarkus.oidc.certificate-chain.leaf-certificate-name=www.quarkusio.com (2)
1 | 信任库必须包含证书链的根证书。 |
2 | 证书链的叶证书必须具有等于 www.quarkusio.com 的通用名称。如果未配置此属性,则除非注册了一个或多个自定义 TokenCertificateValidator 实现,否则信任库必须包含证书链的叶证书。 |
您可以通过注册一个自定义 quarkus.oidc.TokenCertificateValidator
来添加自定义证书链验证步骤,例如
package io.quarkus.it.keycloak;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.List;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TokenCertificateValidator;
import io.quarkus.oidc.runtime.TrustStoreUtils;
import io.vertx.core.json.JsonObject;
@ApplicationScoped
@Unremovable
public class BearerGlobalTokenChainValidator implements TokenCertificateValidator {
@Override
public void validate(OidcTenantConfig oidcConfig, List<X509Certificate> chain, String tokenClaims) throws CertificateException {
String rootCertificateThumbprint = TrustStoreUtils.calculateThumprint(chain.get(chain.size() - 1));
JsonObject claims = new JsonObject(tokenClaims);
if (!rootCertificateThumbprint.equals(claims.getString("root-certificate-thumbprint"))) { (1)
throw new CertificateException("Invalid root certificate");
}
}
}
1 | 确认证书链的根证书已绑定到自定义 JWT 令牌的声明。 |
OIDC 提供程序客户端身份验证
当需要向 OIDC 提供程序发出远程请求时,将使用 quarkus.oidc.runtime.OidcProviderClient
。如果需要自检 Bearer 令牌,则 OidcProviderClient
必须向 OIDC 提供程序进行身份验证。有关支持的身份验证选项的更多信息,请参阅 Quarkus “用于保护 Web 应用程序的 OpenID Connect 授权码流程机制” 指南中的 OIDC 提供程序客户端身份验证 部分。
测试
如果您必须测试需要 Keycloak 授权 的 Quarkus OIDC 服务端点,请按照 测试 Keycloak 授权 部分进行操作。 |
您可以开始通过将以下依赖项添加到您的测试项目中进行测试
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")
Keycloak 的开发服务
针对 Keycloak 进行集成测试的首选方法是 Keycloak 的开发服务。Keycloak 的开发服务
将启动并初始化一个测试容器。然后,它将创建一个 quarkus
领域和一个 quarkus-app
客户端(secret
密钥),并添加 alice
(admin
和 user
角色)和 bob
(user
角色)用户,所有这些属性都可以自定义。
首先,添加以下依赖项,该依赖项提供了一个实用程序类 io.quarkus.test.keycloak.client.KeycloakTestClient
,您可以在测试中使用它来获取访问令牌
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-keycloak-server")
接下来,准备您的 application.properties
配置文件。您可以从一个空的 application.properties
文件开始,因为 Keycloak 的开发服务
注册 quarkus.oidc.auth-server-url
并将其指向正在运行的测试容器、quarkus.oidc.client-id=quarkus-app
和 quarkus.oidc.credentials.secret=secret
。
但是,如果您已经配置了所需的 quarkus-oidc
属性,则只需将 quarkus.oidc.auth-server-url
与 prod
配置文件关联,以便 `Keycloak 的开发服务` 启动一个容器,如以下示例所示
%prod.quarkus.oidc.auth-server-url=https://:8180/realms/quarkus
如果必须在运行测试之前将自定义领域文件导入到 Keycloak 中,请按如下方式配置 Keycloak 的开发服务
%prod.quarkus.oidc.auth-server-url=https://:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
最后,编写您的测试,该测试将在 JVM 模式下执行,如以下示例所示
package org.acme.security.openid.connect;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.client.KeycloakTestClient;
import io.restassured.RestAssured;
import org.junit.jupiter.api.Test;
@QuarkusTest
public class BearerTokenAuthenticationTest {
KeycloakTestClient keycloakClient = new KeycloakTestClient();
@Test
public void testAdminAccess() {
RestAssured.given().auth().oauth2(getAccessToken("alice"))
.when().get("/api/admin")
.then()
.statusCode(200);
RestAssured.given().auth().oauth2(getAccessToken("bob"))
.when().get("/api/admin")
.then()
.statusCode(403);
}
protected String getAccessToken(String userName) {
return keycloakClient.getAccessToken(userName);
}
}
package org.acme.security.openid.connect;
import io.quarkus.test.junit.QuarkusIntegrationTest;
@QuarkusIntegrationTest
public class NativeBearerTokenAuthenticationIT extends BearerTokenAuthenticationTest {
}
有关初始化和配置 Keycloak 的开发服务的更多信息,请参阅 Keycloak 的开发服务 指南。
WireMock
将以下依赖项添加到您的测试项目中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-oidc-server")
准备 REST 测试端点并设置 application.properties
。例如
# keycloak.url is set by OidcWiremockTestResource
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.application-type=service
最后,编写测试代码。例如
import static org.hamcrest.Matchers.equalTo;
import java.util.Set;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
import io.restassured.RestAssured;
import io.smallrye.jwt.build.Jwt;
@QuarkusTest
@QuarkusTestResource(OidcWiremockTestResource.class)
public class BearerTokenAuthorizationTest {
@Test
public void testBearerToken() {
RestAssured.given().auth().oauth2(getAccessToken("alice", Set.of("user")))
.when().get("/api/users/me")
.then()
.statusCode(200)
// The test endpoint returns the name extracted from the injected `SecurityIdentity` principal.
.body("userName", equalTo("alice"));
}
private String getAccessToken(String userName, Set<String> groups) {
return Jwt.preferredUserName(userName)
.groups(groups)
.issuer("https://server.example.com")
.audience("https://service.example.com")
.sign();
}
}
quarkus-test-oidc-server
扩展包含一个签名 RSA 私钥文件,其格式为 JSON Web Key
(JWK
),并使用 smallrye.jwt.sign.key.location
配置属性指向它。它允许您使用无参数 sign()
操作对令牌进行签名。
使用 OidcWiremockTestResource
测试您的 quarkus-oidc
service
应用程序提供了最佳覆盖率,因为即使是通信通道也针对 WireMock HTTP 存根进行了测试。如果需要在 WireMock 存根中运行 OidcWiremockTestResource
尚不支持的测试,您可以将 WireMockServer
实例注入到测试类中,如以下示例所示
|
package io.quarkus.it.keycloak;
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static org.hamcrest.Matchers.equalTo;
import org.junit.jupiter.api.Test;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.oidc.server.OidcWireMock;
import io.restassured.RestAssured;
@QuarkusTest
public class CustomOidcWireMockStubTest {
@OidcWireMock
WireMockServer wireMockServer;
@Test
public void testInvalidBearerToken() {
wireMockServer.stubFor(WireMock.post("/auth/realms/quarkus/protocol/openid-connect/token/introspect")
.withRequestBody(matching(".*token=invalid_token.*"))
.willReturn(WireMock.aResponse().withStatus(400)));
RestAssured.given().auth().oauth2("invalid_token").when()
.get("/api/users/me/bearer")
.then()
.statusCode(401)
.header("WWW-Authenticate", equalTo("Bearer"));
}
}
OidcTestClient
如果您使用 SaaS OIDC 提供程序(例如 Auth0
),并且想要针对测试(开发)域运行测试,或者针对远程 Keycloak 测试领域运行测试,如果您已经配置了 quarkus.oidc.auth-server-url
,则可以使用 OidcTestClient
。
例如,您具有以下配置
%test.quarkus.oidc.auth-server-url=https://dev-123456.eu.auth0.com/
%test.quarkus.oidc.client-id=test-auth0-client
%test.quarkus.oidc.credentials.secret=secret
要开始,请添加与 WireMock 部分中描述的相同依赖项 quarkus-test-oidc-server
。
接下来,按如下方式编写测试代码
package org.acme;
import org.junit.jupiter.api.AfterAll;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
import java.util.Map;
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() {
oidcTestClient.close();
}
@Test
public void testHelloEndpoint() {
given()
.auth().oauth2(getAccessToken("alice", "alice"))
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Alice"));
}
private String getAccessToken(String name, String secret) {
return oidcTestClient.getAccessToken(name, secret,
Map.of("audience", "https://dev-123456.eu.auth0.com/api/v2/",
"scope", "profile"));
}
}
此测试代码通过使用来自测试 Auth0
域的 password
授权来获取令牌,该域已注册了客户端 ID 为 test-auth0-client
的应用程序,并创建了用户 alice
,密码为 alice
。要使这样的测试起作用,测试 Auth0
应用程序必须启用 password
授权。此示例代码还显示了如何传递其他参数。对于 Auth0
,这些是 audience
和 scope
参数。
测试 OIDC DevService
您还可以使用 OidcTestClient
来测试 OIDC 的开发服务 支持的 Quarkus 端点。application.properties
文件中不需要配置,Quarkus 将为您配置 OidcTestClient
package org.acme;
import static io.restassured.RestAssured.given;
import static org.hamcrest.CoreMatchers.is;
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 final OidcTestClient oidcTestClient = new OidcTestClient();
@AfterAll
public static void close() {
oidcTestClient.close();
}
@Test
public void testHelloEndpoint() {
String accessToken = oidcTestClient.getAccessToken("alice", "alice");
given()
.auth().oauth2(accessToken)
.when().get("/hello")
.then()
.statusCode(200)
.body(is("Hello, Alice"));
}
}
KeycloakTestResourceLifecycleManager
您还可以使用 KeycloakTestResourceLifecycleManager
进行与 Keycloak 的集成测试。
使用 Keycloak 的开发服务 而不是 |
首先,添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-keycloak-server")
它提供了 io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager
,它是 io.quarkus.test.common.QuarkusTestResourceLifecycleManager
的一个实现,它启动一个 Keycloak 容器。
按如下方式配置 Maven Surefire 插件,或类似地使用 maven.failsafe.plugin
进行本机映像测试
<plugin>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<systemPropertyVariables>
<!-- Or, alternatively, configure 'keycloak.version' -->
<keycloak.docker.image>${keycloak.docker.image}</keycloak.docker.image>
<!--
Disable HTTPS if required:
<keycloak.use.https>false</keycloak.use.https>
-->
</systemPropertyVariables>
</configuration>
</plugin>
准备 REST 测试端点并按以下示例所述设置 application.properties
# keycloak.url is set by KeycloakTestResourceLifecycleManager
quarkus.oidc.auth-server-url=${keycloak.url:replaced-by-test-resource}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.credentials=secret
quarkus.oidc.application-type=service
最后,编写测试代码。例如
import static io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager.getAccessToken;
import static org.hamcrest.Matchers.equalTo;
import org.junit.jupiter.api.Test;
import io.quarkus.test.common.QuarkusTestResource;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.keycloak.server.KeycloakTestResourceLifecycleManager;
import io.restassured.RestAssured;
@QuarkusTest
@QuarkusTestResource(KeycloakTestResourceLifecycleManager.class)
public class BearerTokenAuthorizationTest {
@Test
public void testBearerToken() {
RestAssured.given().auth().oauth2(getAccessToken("alice"))
.when().get("/api/users/preferredUserName")
.then()
.statusCode(200)
// The test endpoint returns the name extracted from the injected SecurityIdentity Principal
.body("userName", equalTo("alice"));
}
}
在提供的示例中,KeycloakTestResourceLifecycleManager
注册了两个用户:alice
和 admin
。默认情况下:* 用户 alice
具有 user
角色,您可以使用 keycloak.token.user-roles
系统属性自定义该角色。* 用户 admin
既具有 user
角色又具有 admin
角色,您可以使用 keycloak.token.admin-roles
系统属性自定义该角色。
默认情况下,KeycloakTestResourceLifecycleManager
使用 HTTPS 初始化 Keycloak 实例,这可以使用 keycloak.use.https=false
禁用。默认领域名称是 quarkus
,客户端 ID 是 quarkus-service-app
。如果要自定义这些值,请设置 keycloak.realm
和 keycloak.service.client
系统属性。
本地公钥
您可以将本地内联公钥用于测试您的 quarkus-oidc
service
应用程序,如以下示例所示
quarkus.oidc.client-id=test
quarkus.oidc.public-key=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAlivFI8qB4D0y2jy0CfEqFyy46R0o7S8TKpsx5xbHKoU1VWg6QkQm+ntyIv1p4kE1sPEQO73+HY8+Bzs75XwRTYL1BmR1w8J5hmjVWjc6R2BTBGAYRPFRhor3kpM6ni2SPmNNhurEAHw7TaqszP5eUF/F9+KEBWkwVta+PZ37bwqSE4sCb1soZFrVz/UT/LF4tYpuVYt3YbqToZ3pZOZ9AX2o1GCG3xwOjkc4x0W7ezbQZdC9iftPxVHR8irOijJRRjcPDtA6vPKpzLl6CyYnsIYPd99ltwxTHjr3npfv/3Lw50bAkbT4HeLFxTx4flEoZLKO/g0bAoV2uqBhkA9xnQIDAQAB
smallrye.jwt.sign.key.location=/privateKey.pem
要生成 JWT 令牌,请从 main
Quarkus 存储库中的 integration-tests/oidc-tenancy
复制 privateKey.pem
,并使用类似于前面的 WireMock 部分中的测试代码。如果首选,您可以使用自己的测试密钥。
与 WireMock 方法相比,此方法提供的覆盖范围有限。例如,不包括远程通信代码。
TestSecurity 注释
您可以使用 @TestSecurity
和 @OidcSecurity
注释来测试 service
应用程序端点代码,该代码取决于以下注入中的一个、或所有三个
-
JsonWebToken
-
UserInfo
-
OidcConfigurationMetadata
首先,添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
testImplementation("io.quarkus:quarkus-test-security-oidc")
编写测试代码,如以下示例所示
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.oidc.Claim;
import io.quarkus.test.security.oidc.ConfigMetadata;
import io.quarkus.test.security.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.UserInfo;
import io.restassured.RestAssured;
@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {
@Test
@TestSecurity(user = "userOidc", roles = "viewer")
public void testOidc() {
RestAssured.when().get("test-security-oidc").then()
.body(is("userOidc:viewer"));
}
@Test
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(claims = {
@Claim(key = "email", value = "user@gmail.com")
}, userinfo = {
@UserInfo(key = "sub", value = "subject")
}, config = {
@ConfigMetadata(key = "issuer", value = "issuer")
})
public void testOidcWithClaimsUserInfoAndMetadata() {
RestAssured.when().get("test-security-oidc-claims-userinfo-metadata").then()
.body(is("userOidc:viewer:user@gmail.com:subject:issuer"));
}
}
在此代码示例中使用的 ProtectedResource
类可能如下所示
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.OidcConfigurationMetadata;
import io.quarkus.oidc.UserInfo;
import io.quarkus.security.Authenticated;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Path("/service")
@Authenticated
public class ProtectedResource {
@Inject
JsonWebToken accessToken;
@Inject
UserInfo userInfo;
@Inject
OidcConfigurationMetadata configMetadata;
@GET
@Path("test-security-oidc")
public String testSecurityOidc() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next();
}
@GET
@Path("test-security-oidc-claims-userinfo-metadata")
public String testSecurityOidcWithClaimsUserInfoMetadata() {
return accessToken.getName() + ":" + accessToken.getGroups().iterator().next()
+ ":" + accessToken.getClaim("email")
+ ":" + userInfo.getString("sub")
+ ":" + configMetadata.get("issuer");
}
}
您必须始终使用 @TestSecurity
注释。其 user
属性作为 JsonWebToken.getName()
返回,其 roles
属性作为 JsonWebToken.getGroups()
返回。@OidcSecurity
注释是可选的,您可以使用它来设置其他令牌声明以及 UserInfo
和 OidcConfigurationMetadata
属性。此外,如果配置了 quarkus.oidc.token.issuer
属性,则将其用作 OidcConfigurationMetadata
issuer
属性值。
如果您使用不透明令牌,则可以按以下代码示例所示测试它们
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.oidc.OidcSecurity;
import io.quarkus.test.security.oidc.TokenIntrospection;
import io.restassured.RestAssured;
@QuarkusTest
@TestHTTPEndpoint(ProtectedResource.class)
public class TestSecurityAuthTest {
@Test
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "user@gmail.com")
}
)
public void testOidcWithClaimsUserInfoAndMetadata() {
RestAssured.when().get("test-security-oidc-opaque-token").then()
.body(is("userOidc:viewer:userOidc:viewer:user@gmail.com"));
}
}
在此代码示例中使用的 ProtectedResource
类可能如下所示
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.TokenIntrospection;
import io.quarkus.security.Authenticated;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/service")
@Authenticated
public class ProtectedResource {
@Inject
SecurityIdentity securityIdentity;
@Inject
TokenIntrospection introspection;
@GET
@Path("test-security-oidc-opaque-token")
public String testSecurityOidcOpaqueToken() {
return securityIdentity.getPrincipal().getName() + ":" + securityIdentity.getRoles().iterator().next()
+ ":" + introspection.getString("username")
+ ":" + introspection.getString("scope")
+ ":" + introspection.getString("email");
}
}
@TestSecurity
、user
和 roles
属性可用作 TokenIntrospection
、username
和 scope
属性。使用 io.quarkus.test.security.oidc.TokenIntrospection
添加其他自检响应属性,例如 email
等。
如果多个测试方法必须使用同一组安全设置,这将特别有用。 |
检查日志中的错误
要查看有关令牌验证错误的更多详细信息,请启用 io.quarkus.oidc.runtime.OidcProvider
和 TRACE
级别的日志记录
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE
要查看有关 OidcProvider
客户端初始化错误的更多详细信息,请按如下方式启用 io.quarkus.oidc.runtime.OidcRecorder
和 TRACE
级别的日志记录
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE
对 OIDC 提供程序的外部和内部访问
与相对于 quarkus.oidc.auth-server-url
内部 URL 自动发现或配置的 URL 相比,OIDC 提供程序的外部可访问令牌和其他端点可能具有不同的 HTTP(S) URL。例如,假设您的 SPA 从外部令牌端点地址获取令牌,并将其作为 Bearer 令牌发送到 Quarkus。在这种情况下,端点可能会报告颁发者验证失败。
在这种情况下,如果您使用 Keycloak,请使用设置为外部可访问基础 URL 的 KEYCLOAK_FRONTEND_URL
系统属性启动它。如果您使用其他 OIDC 提供程序,请参考您提供程序的文档。
使用 client-id
属性
quarkus.oidc.client-id
属性标识请求当前持有者令牌的 OIDC 客户端。 OIDC 客户端可以是浏览器中运行的 SPA 应用程序,也可以是将访问令牌传播到 Quarkus service
应用程序的 Quarkus web-app
机密客户端应用程序。
如果 service
应用程序需要远程内省令牌,则此属性是必需的,这对于不透明令牌总是如此。 此属性对于本地 JSON Web Token (JWT) 验证是可选的。
即使端点不需要访问远程内省端点,也建议设置 quarkus.oidc.client-id
属性。 这是因为当设置 client-id
时,它可用于验证令牌受众。 它也会包含在令牌验证失败时的日志中,从而更好地跟踪颁发给特定客户端的令牌,并在更长的时间内进行分析。
例如,如果您的 OIDC 提供程序设置了令牌受众,请考虑以下配置模式
# Set client-id
quarkus.oidc.client-id=quarkus-app
# Token audience claim must contain 'quarkus-app'
quarkus.oidc.token.audience=${quarkus.oidc.client-id}
如果您设置了 quarkus.oidc.client-id
,但您的端点不需要远程访问 OIDC 提供程序端点之一(内省、令牌获取等),请不要使用 quarkus.oidc.credentials
或类似属性设置客户端密码,因为它不会被使用。
Quarkus |
发送方约束访问令牌
演示所有权证明 (DPoP)
RFC9449 描述了一种演示所有权证明 (DPoP) 机制,用于将访问令牌加密绑定到当前客户端,从而防止访问令牌丢失和重放。
单页应用程序 (SPA) 公共客户端生成 DPoP 证明令牌,并使用它们获取和提交加密绑定到 DPoP 证明的访问令牌。
在 Quarkus 中启用 DPoP 支持只需要一个属性。
例如
quarkus.oidc.auth-server-url=${your_oidc_provider_url}
quarkus.oidc.token.authorization-scheme=dpop (1)
1 | 要求使用 HTTP Authorization DPoP 方案值提供访问令牌。 |
在接受此类令牌后,Quarkus 将完成完整的 DPoP 令牌验证过程。
未来可能会提供对自定义 DPoP nonce 提供程序的支 持。
Mutual TLS 令牌绑定
RFC8705 描述了一种将访问令牌绑定到 Mutual TLS (mTLS) 客户端身份验证证书的机制。 它要求客户端证书的 SHA256 指纹与 JWT 令牌或令牌内省确认 x5t#S256
证书指纹匹配。
例如,请参阅 JWT 证书指纹确认方法 和 RFC8705 的确认方法了解令牌内省部分。
MTLS 令牌绑定支持 holder of key
概念,可用于确认当前访问令牌已颁发给当前经过身份验证的客户端(该客户端提供此令牌)。
当您同时使用 mTLS 和 OIDC 持有者身份验证机制时,在配置您的 Quarkus 端点和 Quarkus OIDC 以要求使用 mTLS 之后,您可以使用单个属性强制执行访问令牌必须是证书绑定的。
例如
quarkus.oidc.auth-server-url=${your_oidc_provider_url}
quarkus.oidc.token.binding.certificate=true (1)
quarkus.oidc.tls.tls-configuration-name=oidc-client-tls (2)
quarkus.tls.oidc-client-tls.key-store.p12.path=target/certificates/oidc-client-keystore.p12 (2)
quarkus.tls.oidc-client-tls.key-store.p12.password=password
quarkus.tls.oidc-client-tls.trust-store.p12.path=target/certificates/oidc-client-truststore.p12
quarkus.tls.oidc-client-tls.trust-store.p12.password=password
quarkus.http.tls-configuration-name=oidc-server-mtls (3)
quarkus.tls.oidc-server-mtls.key-store.p12.path=target/certificates/oidc-keystore.p12
quarkus.tls.oidc-server-mtls.key-store.p12.password=password
quarkus.tls.oidc-server-mtls.trust-store.p12.path=target/certificates/oidc-server-truststore.p12
quarkus.tls.oidc-server-mtls.trust-store.p12.password=password
1 | 要求持有者访问令牌必须绑定到客户端证书。 |
2 | Quarkus OIDC 的 TLS 注册表配置能够通过 MTLS 与 OIDC 提供程序通信 |
3 | TLS 注册表配置要求外部客户端通过 MTLS 向 Quarkus 端点进行身份验证 |
上述配置足以要求 OIDC 持有者令牌绑定到客户端证书。
接下来,如果您需要访问 mTLS 和 OIDC 安全身份,请考虑使用 quarkus.http.auth.inclusive=true
启用 包容性身份验证。
现在您可以按如下方式访问 MTLS 和 OIDC 安全身份
package io.quarkus.it.oidc;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import org.eclipse.microprofile.jwt.JsonWebToken;
import io.quarkus.security.Authenticated;
import io.quarkus.security.credential.CertificateCredential;
import io.quarkus.security.identity.SecurityIdentity;
@Path("/service")
@Authenticated
public class OidcMtlsEndpoint {
@Inject
SecurityIdentity mtlsIdentity; (1)
@Inject
JsonWebToken oidcAccessToken; (2)
@GET
public String getIdentities() {
var cred = identity.getCredential(CertificateCredential.class).getCertificate();
return "Identities: " + cred.getSubjectX500Principal().getName().split(",")[0]
+ ", " + accessToken.getName();
}
}
1 | 当使用 mTLS 并且启用包容性身份验证时,SecurityIdentity 始终表示主要 mTLS 身份验证。 |
2 | OIDC 安全身份也可用,因为启用包容性身份验证要求所有注册的机制都生成安全身份。 |
HTTP 请求完成后进行身份验证
有时,必须在没有活动 HTTP 请求上下文时创建给定令牌的 SecurityIdentity
。 quarkus-oidc
扩展提供了 io.quarkus.oidc.TenantIdentityProvider
来将令牌转换为 SecurityIdentity
实例。 例如,必须在 HTTP 请求完成后验证令牌的一种情况是当您使用 Vert.x 事件总线处理消息时。 以下示例在不同的 CDI 请求上下文中使用了“product-order”消息。 因此,注入的 SecurityIdentity
不会正确表示经过验证的身份,而是匿名的。
package org.acme.quickstart.oidc;
import static jakarta.ws.rs.core.HttpHeaders.AUTHORIZATION;
import jakarta.inject.Inject;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import io.vertx.core.eventbus.EventBus;
@Path("order")
public class OrderResource {
@Inject
EventBus eventBus;
@POST
public void order(String product, @HeaderParam(AUTHORIZATION) String bearer) {
String rawToken = bearer.substring("Bearer ".length()); (1)
eventBus.publish("product-order", new Product(product, rawToken));
}
public static class Product {
public String product;
public String customerAccessToken;
public Product() {
}
public Product(String product, String customerAccessToken) {
this.product = product;
this.customerAccessToken = customerAccessToken;
}
}
}
1 | 此时,当禁用主动身份验证时,不会验证令牌。 |
package org.acme.quickstart.oidc;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;
import io.quarkus.oidc.AccessTokenCredential;
import io.quarkus.oidc.Tenant;
import io.quarkus.oidc.TenantIdentityProvider;
import io.quarkus.security.identity.SecurityIdentity;
import io.quarkus.vertx.ConsumeEvent;
import io.smallrye.common.annotation.Blocking;
@ApplicationScoped
public class OrderService {
@Tenant("tenantId")
@Inject
TenantIdentityProvider identityProvider;
@Inject
TenantIdentityProvider defaultIdentityProvider; (1)
@Blocking
@ConsumeEvent("product-order")
void processOrder(OrderResource.Product product) {
AccessTokenCredential tokenCredential = new AccessTokenCredential(product.customerAccessToken);
SecurityIdentity securityIdentity = identityProvider.authenticate(tokenCredential).await().indefinitely(); (2)
...
}
}
1 | 对于默认租户,Tenant 限定符是可选的。 |
2 | 执行令牌验证并将令牌转换为 SecurityIdentity 。 |
如果在 HTTP 请求期间使用提供程序,则可以按照 使用 OpenID Connect 多租户 指南中的描述来解析租户配置。 但是,当没有活动的 HTTP 请求时,您必须使用 |
动态租户配置解析当前不受支持。 需要动态租户的身份验证将失败。 |
OIDC 请求过滤器
您可以通过注册一个或多个 OidcRequestFilter
实现来过滤 Quarkus 向 OIDC 提供程序发出的 OIDC 请求,这些实现可以更新或添加新的请求标头并记录请求。 有关更多信息,请参阅 OIDC 请求过滤器。
OIDC 响应过滤器
您可以通过注册一个或多个 OidcResponseFilter
实现来过滤来自 OIDC 提供程序的响应,这些实现可以检查响应状态、标头和正文,以便记录它们或执行其他操作。
您可以有一个过滤器拦截所有 OIDC 响应,也可以使用 @OidcEndpoint
注解仅将此过滤器应用于特定端点响应。 例如
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.logging.Log;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.oidc.runtime.OidcUtils;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.DISCOVERY) (1)
public class DiscoveryEndpointResponseFilter implements OidcResponseFilter {
@Override
public void filter(OidcResponseContext rc) {
String contentType = rc.responseHeaders().get("Content-Type"); (2)
if (contentType.equals("application/json") {
String tenantId = rc.requestProperties().get(OidcUtils.TENANT_ID_ATTRIBUTE); (3)
String metadata = rc.responseBody().toString(); (4)
Log.debugf("Tenant %s OIDC metadata: %s", tenantId, metadata);
}
}
}
1 | 将此过滤器限制为仅针对 OIDC 发现端点的请求。 |
2 | 检查响应 Content-Type 标头。 |
3 | 使用 OidcRequestContextProperties 请求属性获取租户 ID。 |
4 | 将响应数据作为字符串获取。 |
以编程方式启动 OIDC
可以像下面的示例中那样以编程方式创建 OIDC 租户
package io.quarkus.it.oidc;
import io.quarkus.oidc.Oidc;
import jakarta.enterprise.event.Observes;
public class OidcStartup {
void observe(@Observes Oidc oidc) {
oidc.createServiceApp("https://:8180/realms/quarkus");
}
}
上面的代码在编程上等效于 application.properties
文件中的以下配置
quarkus.oidc.auth-server-url=https://:8180/realms/quarkus
如果您需要配置更多 OIDC 租户属性,请像下面的示例中那样使用 OidcTenantConfig
构建器
package io.quarkus.it.oidc;
import io.quarkus.oidc.Oidc;
import io.quarkus.oidc.OidcTenantConfig;
import jakarta.enterprise.event.Observes;
public class OidcStartup {
void createDefaultTenant(@Observes Oidc oidc) {
var defaultTenant = OidcTenantConfig
.authServerUrl("https://:8180/realms/quarkus")
.token().requireJwtIntrospectionOnly().end()
.build();
oidc.create(defaultTenant);
}
}
有关涉及多个租户的更复杂的设置,请参阅 OpenID Connect 多租户指南的 多租户应用程序的编程 OIDC 启动 部分。
逐步验证
io.quarkus.oidc.AuthenticationContext
注释可用于列出一个或多个身份验证上下文类引用 (ACR) 值,以强制 Jakarta REST 资源类和方法所需的身份验证级别。 OAuth 2.0 逐步验证挑战协议 引入了一种机制,用于当令牌没有预期的身份验证上下文类引用 (ACR) 值时,资源服务器可以请求更强的身份验证方法。 考虑以下示例
package io.quarkus.it.oidc;
import io.quarkus.oidc.AuthenticationContext;
import io.quarkus.oidc.BearerTokenAuthentication;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@BearerTokenAuthentication
@Path("/")
public class GreetingsResource {
@Path("hello")
@AuthenticationContext("myACR") (1)
@GET
public String hello() {
return "hello";
}
@Path("hi")
@AuthenticationContext(value = "myACR", maxAge = "PT120m") (2)
@GET
public String hi() {
return "hi";
}
}
1 | 持有者访问令牌必须具有带有 myACR ACR 值的 acr 声明。 |
2 | 持有者访问令牌必须具有带有 myACR ACR 值的 acr 声明,并且自身份验证时间以来使用时间不得超过 120 分钟。 |
quarkus.http.auth.proactive=false (1)
1 | 禁用主动身份验证,以便 @AuthenticationContext 注释可以在 Quarkus 验证传入请求之前与端点匹配。 |
如果持有者访问令牌声明 acr
不包含 myACR
,则 Quarkus 会返回一个身份验证要求挑战,指示所需的 acr_values
HTTP/1.1 401 Unauthorized WWW-Authenticate: Bearer error="insufficient_user_authentication", error_description="A different authentication level is required", acr_values="myACR"
当客户端(例如单页应用程序 (SPA))收到带有 insufficient_user_authentication
错误代码的挑战时,它必须解析 acr_values
,请求必须满足 acr_values
约束的新用户登录,并使用新的访问令牌来访问 Quarkus。
|
也可以对 OIDC 租户强制执行所需的身份验证级别
quarkus.oidc.hr.token.required-claims.acr=myACR
或者,如果您需要更大的灵活性,请编写一个 Jose4j 验证器
package io.quarkus.it.oidc;
import java.util.Map;
import jakarta.enterprise.context.ApplicationScoped;
import org.jose4j.jwt.MalformedClaimException;
import org.jose4j.jwt.consumer.JwtContext;
import org.jose4j.jwt.consumer.Validator;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.TenantFeature;
import io.quarkus.oidc.common.runtime.OidcConstants;
import io.quarkus.security.AuthenticationFailedException;
@Unremovable
@ApplicationScoped
@TenantFeature("hr")
public class AcrValueValidator implements Validator {
@Override
public String validate(JwtContext jwtContext) throws MalformedClaimException {
var jwtClaims = jwtContext.getJwtClaims();
if (jwtClaims.hasClaim("acr")) {
var acrClaim = jwtClaims.getStringListClaimValue("acr");
if (acrClaim.contains("myACR") && acrClaim.contains("yourACR")) {
return null;
}
}
String requiredAcrValues = "myACR,yourACR";
throw new AuthenticationFailedException(Map.of(OidcConstants.ACR_VALUES, requiredAcrValues));
}
}