第 1 章 OpenID Connect (OIDC) Bearer 令牌身份验证
通过使用 Quarkus OpenID Connect (OIDC)扩展,保护应用中对带有 Bearer 令牌身份验证的 Jakarta REST (以前称为 JAX-RS)端点的 HTTP 访问。
1.1. Quarkus 中的 Bearer 令牌身份验证机制概述
Quarkus 通过 Quarkus OpenID Connect (OIDC)扩展支持 Bearer 令牌身份验证机制。
bearer 令牌由 OIDC 和 OAuth 2.0 兼容授权服务器发布,如 Keycloak。
bearer 令牌身份验证是根据 bearer 令牌存在和有效授权 HTTP 请求的过程。bearer 令牌提供有关调用主题的信息,用于确定是否可以访问 HTTP 资源。
下图显示了 Quarkus 中的 Bearer 令牌身份验证机制:
图 1.1. 带有单页应用程序的 Quarkus 中的 bearer 令牌身份验证机制
![bearer 令牌身份验证](https://access.redhat.com/webassets/avalon/d/Red_Hat_build_of_Quarkus-3.15-OpenID_Connect_OIDC_authentication-zh-CN/images/ee57dabe622c0af96f4fd4fcef3e30d4/security-bearer-token-authorization-mechanism-1.png)
- Quarkus 服务从 OIDC 供应商检索验证密钥。验证密钥用于验证 bearer 访问令牌签名。
- Quarkus 用户访问单页应用程序(SPA)。
- 单页应用程序使用授权代码流来验证用户并从 OIDC 供应商检索令牌。
- 单页应用使用访问令牌从 Quarkus 服务检索服务数据。
- Quarkus 服务使用验证密钥验证 bearer 访问令牌签名,检查令牌到期日期和其他声明,允许请求在令牌有效时继续,并将服务响应返回到单页应用。
- 单页应用程序将同一数据返回到 Quarkus 用户。
图 1.2. 使用 Java 或命令行客户端的 Quarkus 中的 bearer 令牌身份验证机制
![bearer 令牌身份验证](https://access.redhat.com/webassets/avalon/d/Red_Hat_build_of_Quarkus-3.15-OpenID_Connect_OIDC_authentication-zh-CN/images/721a0096e56142437ce70f470e75f41c/security-bearer-token-authorization-mechanism-2.png)
- Quarkus 服务从 OIDC 供应商检索验证密钥。验证密钥用于验证 bearer 访问令牌签名。
-
客户端使用
client_credentials
,它需要客户端 ID 和 secret 或密码授权,这需要客户端 ID、secret、用户名和密码从 OIDC 供应商检索访问令牌。 - 客户端使用访问令牌从 Quarkus 服务检索服务数据。
- Quarkus 服务使用验证密钥验证 bearer 访问令牌签名,检查令牌到期日期和其他声明,允许请求在令牌有效时继续,并将服务响应返回给客户端。
如果您需要使用 OIDC 授权代码流验证和授权用户,请参阅 Quarkus OpenID Connect 授权代码流机制来保护 Web 应用程序 指南。另外,如果您使用 Keycloak 和 bearer 令牌,请参阅使用 Keycloak 的 Quarkus 来集中授权 指南。
要了解如何使用 OIDC Bearer 令牌身份验证来保护服务应用程序,请参阅以下教程:
有关如何支持多个租户的详情,请参考使用 OpenID Connect Multi-Tenancy 的 Quarkus。
1.1.1. 访问 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"; } }
@ApplicationScoped
、@Singleton
和 @RequestScoped
范围支持 JsonWebToken
注入。但是,如果单个声明被注入为简单类型,则需要使用 @RequestScoped
。如需更多信息,请参阅 Quarkus "Using JWT RBAC" 指南中的 支持的注入范围 部分。
1.1.2. UserInfo
如果您必须从 OIDC UserInfo
端点请求 UserInfo JSON 对象,请设置 quarkus.oidc.authentication.user-info-required=true
。请求发送到 OIDC 提供程序 UserInfo
端点,并且创建 io.quarkus.oidc.UserInfo
(一个简单的 javax.json.JsonObject
wrapper)对象。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 端点时才检测到。
1.1.3. 配置元数据
当前租户的发现的 OpenID Connect 配置元数据 由 io.quarkus.oidc.OidcConfigurationMetadata
表示,并可作为 SecurityIdentity
configuration-metadata
属性注入或访问。
如果端点是 public,则默认租户的 OidcConfigurationMetadata
会被注入。
1.1.4. 令牌声明和安全身份角色
您可以从验证的 JWT 访问令牌映射 SecurityIdentity
角色,如下所示:
-
如果设置了
quarkus.oidc.roles.role-claim-path
属性,并找到匹配的数组或字符串声明,则从这些声明中提取角色。例如,Customroles,
,custom
roles/arrayscope
,"http://namespace-qualified-custom-claim"/roles
,"http://namespace-qualified-roles"
。 -
如果有一个
组
声明可用,则使用其值。 -
如果
realm_access/roles
或resource_access/client_id/roles
(其中client_id
是quarkus.oidc.client-id
属性的值)声明可用,则使用其值。此检查支持 Keycloak 发布的令牌。
例如,以下 JWT 令牌具有一个复杂的 groups
声明,其中包含包含 角色的角色
数组:
{ "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 " Security tips and tricks" 指南中的 安全身份自定义 部分。
您还可以使用 HTTP 安全策略 将创建从令牌声明创建的 SecurityIdentity
角色映射到特定于部署的角色。
1.1.5. 令牌范围和 SecurityIdentity 权限
SecurityIdentity
权限以 io.quarkus.security.StringPermission
的形式映射,来自 角色源的 scope 参数,并使用相同的声明分隔符。
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; } } }
有关 io.quarkus.security.PermissionsAllowed
注解的更多信息,请参阅"Authorization of web endpoint"指南中的 Permission 注解 部分。
1.1.6. 令牌验证和内省
如果令牌是 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=http://localhost:8180/realms/quarkus quarkus.oidc.discovery-enabled=false # Token Introspection endpoint: http://localhost: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=http://localhost:8180/realms/quarkus quarkus.oidc.token.require-jwt-introspection-only=true
这种方法的一个优点是配置更简单且更易于理解。缺点是,远程 OIDC 元数据发现调用需要发现内省端点地址,即使不会获取验证密钥。
将创建 io.quarkus.oidc.TokenIntrospection
,它是一个简单的 jakarta.json.JsonObject
wrapper 对象。它可以作为 SecurityIdentity
introspection
属性注入或访问,提供 JWT 或不透明令牌已成功内省。
1.1.7. 令牌内省和 UserInfo
缓存
所有不透明访问令牌都必须远程内省。有时,还必须内省 JWT 访问令牌。如果同时需要 UserInfo
,则会在对 OIDC 提供程序的后续远程调用中使用相同的访问令牌。因此,如果需要 UserInfo
,并且当前访问令牌不透明,会为每个此类令牌进行两个远程调用;一个远程调用来内省令牌,另一个用于获取 UserInfo
。如果令牌是 JWT,则只需要对 get 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.TokenIntrospection
数据、quarkus.oidc.UserInfo
数据,或使用布尔值 quarkus.oidc."tenant".allow-token-introspection-cache
和 quarkus.oidc."tenant".allow-user-info-cache
属性的存储。
另外,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
或两者。它只会保留最大大小的条目数。如果在添加新条目时缓存已满,则会尝试通过删除单个过期条目来查找空格。另外,如果激活,清理计时器会定期检查过期的条目并删除它们。
您可以使用默认缓存实现试验或注册自定义缓存。
1.1.8. JSON Web 令牌声明验证
在验证 bearer JWT 令牌签名 并在
(exp
)声明被检查后,将验证 iss
(issuer
)声明值。
默认情况下,iss
claim 值与 issuer
属性进行比较,该属性可能已在已知的提供程序配置中发现。但是,如果设置了 quarkus.oidc.token.issuer
属性,则 iss
claim 值会被与它进行比较。
在某些情况下,这是声明
验证可能无法正常工作。例如,如果发现的 issuer
属性包含内部 HTTP/IP 地址,而 令牌是
声明值包含外部 HTTP/IP 地址。或者,当发现的 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()); } } }
考虑使用 quarkus.oidc.token.audience
属性来验证令牌 aud
(audience
)声明值。
1.1.9. Jose4j Validator
您可以注册自定义 Jose4j Validator,以自定义 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 } }
使用 @quarkus.oidc.TenantFeature
注解将自定义 Validator 绑定到特定的 OIDC 租户。
1.1.10. 跨原始资源共享
如果您计划从在不同域上运行的单页应用中使用 OIDC 服务
应用程序,您必须配置跨原始资源共享(CORS)。如需更多信息,请参阅"Cross-origin 资源共享"指南中的 CORS 过滤器 部分。
1.1.11. 供应商端点配置
OIDC 服务
应用需要知道 OIDC 供应商的令牌、JsonWebKey
(JWK)设置,以及可能的 UserInfo
和内省端点地址。
默认情况下,通过将 /.well-known/openid-configuration
路径添加到配置的 quarkus.oidc.auth-server-url
来发现它们。
或者,如果发现端点不可用,或者要在发现端点往返中保存,您可以禁用发现并使用相对路径值进行配置。例如:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.discovery-enabled=false # Token endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/token quarkus.oidc.token-path=/protocol/openid-connect/token # JWK set endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/certs quarkus.oidc.jwks-path=/protocol/openid-connect/certs # UserInfo endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/userinfo quarkus.oidc.user-info-path=/protocol/openid-connect/userinfo # Token Introspection endpoint: http://localhost:8180/realms/quarkus/protocol/openid-connect/tokens/introspect quarkus.oidc.introspection-path=/protocol/openid-connect/tokens/introspect
1.1.12. 令牌传播
有关 bearer 访问令牌传播到下游服务的详情,请参考 Quarkus "OpenID Connect (OIDC)和 OAuth2 客户端和过滤器参考" 指南中的 Token propagation 部分。
1.1.13. 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
您可以通过注册自定义 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 令牌声明。
1.1.14. OIDC 供应商客户端身份验证
当需要对 OIDC 供应商的远程请求时,使用 quarkus.oidc.runtime.OidcProviderClient
。如果需要内省 Bearer 令牌,则 OidcProviderClient
必须向 OIDC 提供程序进行身份验证。有关支持的验证选项的更多信息,请参阅 Quarkus "OpenID Connect authorization code flow mechanism for protect web application" 指南中的 OIDC provider client authentication 部分。
1.1.15. 测试
如果需要测试需要 Keycloak 授权的 Quarkus OIDC 服务端点,请按照 Test Keycloak 授权 部分操作。
您可以通过在测试项目中添加以下依赖项开始测试:
使用 Maven:
<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>
使用 Gradle:
testImplementation("io.rest-assured:rest-assured") testImplementation("io.quarkus:quarkus-junit5")
1.1.15.1. WireMock
在您的测试项目中添加以下依赖项:
使用 Maven:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-oidc-server</artifactId> <scope>test</scope> </dependency>
使用 Gradle:
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
扩展包含 JSON Web
密钥(JWK
)格式的签名 RSA 私钥文件,并使用 smallrye.jwt.sign.key.location
配置属性指向它。它允许您使用 no-argument sign ()
操作为令牌签名。
使用 OidcWiremockTestResource
测试 quarkus-oidc
服务
应用程序提供了最佳覆盖,因为即使通信通道针对 WireMock HTTP stub 进行了测试。如果您需要使用 WireMock stubs 运行测试,它还没有被 OidcWiremockTestResource
支持,您可以将 WireMockServer
实例注入测试类,如下例所示:
OidcWiremockTestResource
无法针对 Docker 容器使用 @QuarkusIntegrationTest
,因为运行测试的 JVM 中运行 WireMock 服务器,这无法从运行 Quarkus 应用的 Docker 容器访问。
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")); } }
1.1.16. 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
要启动,请添加相同的依赖项 quarkus-test-oidc-server
,如 WireMock 部分所述。
接下来,按如下方式编写测试代码:
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")); } }
此测试代码使用来自 test Auth0
域的 密码
授权获取令牌,该密码使用客户端 id test-auth0-client
注册了应用,并使用密码 alice
创建用户 alice
。要使测试正常工作,test Auth0
应用必须启用 密码
授权。这个示例代码还演示了如何传递额外的参数。对于 Auth0
,它们是 audience
和 scope
参数。
1.1.16.1. 用于 Keycloak 的 dev Services
针对 Keycloak 进行集成测试的首选方法是 Keycloak 的 Dev Services。用于 Keycloak 的 dev Services
将启动并初始化测试容器。然后,它将创建一个 quarkus
realm 和 quarkus-app
客户端(secret
secret),并添加 alice
(admin
和
roles)和 user
bob
(用户角色)用户,其中所有这些属性都可以自定义。
首先,添加以下依赖项,它提供实用程序类 io.quarkus.test.keycloak.client.KeycloakTestClient
,您可以用于测试获取访问令牌:
使用 Maven:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-keycloak-server</artifactId> <scope>test</scope> </dependency>
使用 Gradle:
testImplementation("io.quarkus:quarkus-test-keycloak-server")
接下来,准备 application.properties
配置文件。您可以从一个空的 application.properties
文件开始,因为 Keycloak 的 Dev Services
注册 quarkus.oidc.auth-server-url
,并将它指向正在运行的测试容器,quarkus.oidc.client-id=quarkus-app
, 和 quarkus.oidc.credentials.secret=secret
。
但是,如果您已配置了所需的 quarkus-oidc
属性,则您只需要将 quarkus.oidc.auth-server-url
与 'Dev Services for Keycloak' 的 prod
配置集关联,如下例所示:
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
如果在运行测试前必须将自定义域文件导入到 Keycloak 中,请为 Keycloak 配置 Dev Services
,如下所示:
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.keycloak.devservices.realm-path=quarkus-realm.json
最后,编写您的测试,它将在 JVM 模式中执行,如下例所示:
以 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 的 Dev 服务的更多信息,请参阅 Dev Services for Keycloak 指南。
1.1.16.2. 本地公钥
您可以使用本地内联公钥来测试 quarkus-oidc
服务
应用程序,如下例所示:
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 令牌,请从 主
Quarkus 存储库中的 integration-tests/oidc-tenancy
复制 privateKey.pem
,并使用与上一 WireMock 部分中类似的测试代码。如果需要,您可以使用自己的测试密钥。
与 WireMock 方法相比,这种方法提供有限的覆盖范围。例如,不包括远程通信代码。
1.1.16.3. TestSecurity 注解
您可以使用 @TestSecurity
和 @OidcSecurity
注释来测试 服务
应用程序端点代码(取决于以下注入之一或全部三个):
-
JsonWebToken
-
UserInfo
-
OidcConfigurationMetadata
首先,添加以下依赖项:
使用 Maven:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-test-security-oidc</artifactId> <scope>test</scope> </dependency>
使用 Gradle:
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
来添加额外的内省响应属性,如电子邮件
等等。
@TestSecurity
和 @OidcSecurity
可以合并到 meta-annotation 中,如下例所示:
@Retention(RetentionPolicy.RUNTIME) @Target({ ElementType.METHOD }) @TestSecurity(user = "userOidc", roles = "viewer") @OidcSecurity(introspectionRequired = true, introspection = { @TokenIntrospection(key = "email", value = "user@gmail.com") } ) public @interface TestSecurityMetaAnnotation { }
如果多个测试方法必须使用同一组安全设置,这特别有用。
1.1.17. 检查日志中的错误
要查看有关令牌验证错误的更多详细信息,请启用 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
1.1.18. 对 OIDC 供应商的外部和内部访问
与自动发现或配置了 quarkus.oidc.auth-server-url
内部 URL 的 URL 相比,OIDC 供应商和其他端点可能具有不同的 HTTP (S) URL。例如,假设您的 SPA 从外部令牌端点地址获取令牌,并将其发送到 Quarkus 作为 bearer 令牌。在这种情况下,端点可能会报告签发者验证失败。
在这种情况下,如果您使用 Keycloak,使用 KEYCLOAK_FRONTEND_URL
系统属性将其设置为外部可访问的基本 URL。如果使用其他 OIDC 供应商,请参阅您的供应商文档。
1.1.19. 使用 client-id
属性
quarkus.oidc.client-id
属性标识请求当前 bearer 令牌的 OIDC 客户端。OIDC 客户端可以是在浏览器中运行的 SPA 应用程序,也可以是 Quarkus Web-app
机密客户端应用程序将访问令牌传播到 Quarkus 服务
应用程序。
如果服务
应用预期远程内省令牌,则需要此属性,这始终是不透明令牌的情况。此属性是可选的,用于本地 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
或类似属性设置客户端 secret,因为它不会被使用。
Quarkus web-app
应用程序总是需要 quarkus.oidc.client-id
属性。