通过使用 Quarkus OpenID Connect (OIDC)扩展,保护应用中对带有 Bearer 令牌身份验证的 Jakarta REST (以前称为 JAX-RS)端点的 HTTP 访问。
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 和 secret 或密码授权,这需要客户端 ID、secret、用户名和密码从 OIDC 供应商检索访问令牌。
客户端使用访问令牌从 Quarkus 服务检索服务数据。
Quarkus 服务使用验证密钥验证 bearer 访问令牌签名,检查令牌到期日期和其他声明,允许请求在令牌有效时继续,并将服务响应返回给客户端。
如果您需要使用 OIDC 授权代码流验证和授权用户,请参阅 Quarkus OpenID Connect 授权代码流机制来保护 Web 应用程序 指南。另外,如果您使用 Keycloak 和 bearer 令牌,请参阅使用 Keycloak 的 Quarkus 来集中授权 指南。
要了解如何使用 OIDC Bearer 令牌身份验证来保护服务应用程序,请参阅以下教程:* 使用 OpenID Connect (OIDC)授权代码流保护 Web 应用程序 。
有关如何支持多个租户的详情,请参考使用 OpenID Connect Multi-Tenancy 的 Quarkus。
如果需要访问 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";
}
}
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";
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
@ApplicationScoped
、@Singleton
和 @RequestScoped
范围支持 JsonWebToken
注入。但是,如果单个声明被注入为简单类型,则需要使用 @RequestScoped
。如需更多信息,请参阅 Quarkus "Using JWT RBAC" 指南中的 支持的注入范围 部分。
如果您必须从 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
属性注入或访问。
您可以从验证的 JWT 访问令牌映射 SecurityIdentity
角色,如下所示:
如果设置了 quarkus.oidc.roles.role-claim-path
属性,并找到匹配的数组或字符串声明,则从这些声明中提取角色。例如,Customroles ,custom
roles/array
,scope
,"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"
],
}
}
{
"iss": "https://server.example.com",
"sub": "24400320",
"upn": "jdoe@example.com",
"preferred_username": "jdoe",
"exp": 1311281970,
"iat": 1311280970,
"groups": {
"roles": [
"microprofile_jwt_user"
],
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
您必须将 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
角色映射到特定于部署的角色。
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")
@GET
@Path("/email")
public Boolean isUserEmailAddressVerifiedByUser() {
return accessToken.getClaim(Claims.email_verified.name());
}
@PermissionsAllowed("orders_read")
@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;
}
}
}
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;
}
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
1
只有 OpenID Connect 范围 电子邮件
的请求才会被授予访问权限。
2
读访问权限仅限于具有 orders_read
范围的客户端请求。
有关 io.quarkus.security.PermissionsAllowed
注解的更多信息,请参阅"Authorization of web endpoint"指南中的 Permission 注解 部分。
如果令牌是 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
quarkus.oidc.token.allow-jwt-introspection=false
quarkus.oidc.token.allow-opaque-token-introspection=false
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
在某些情况下,只有通过内省验证 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
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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
远程强制 JWT 令牌内省有优缺点。优点是,您可以消除两个远程调用的需要:远程 OIDC 元数据发现调用,以及另一个远程调用来获取不使用的验证密钥。缺点是,您需要知道内省端点地址并手动配置。
另一种方法是允许 OIDC 元数据发现的默认选项,还需要只执行远程 JWT 内省,如下例所示:
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true
quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.token.require-jwt-introspection-only=true
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
这种方法的一个优点是配置更简单且更易于理解。缺点是,远程 OIDC 元数据发现调用需要发现内省端点地址,即使不会获取验证密钥。
将创建 io.quarkus.oidc.TokenIntrospection
,它是一个简单的 jakarta.json.JsonObject
wrapper 对象。它可以作为 SecurityIdentity
introspection
属性注入或访问,提供 JWT 或不透明令牌已成功内省。
所有不透明访问令牌都必须远程内省。有时,还必须内省 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 {
...
}
@ApplicationScoped
@Alternative
@Priority(1)
public class CustomIntrospectionUserInfoCache implements TokenIntrospectionCache, UserInfoCache {
...
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
每个 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:
'time-to-live' specifies how long a cache entry can be valid for and will be used by a cleanup timer:
'clean-up-timer-interval' is not set by default, so the cleanup timer can be activated by setting 'clean-up-timer-interval':
# '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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
默认缓存使用令牌作为密钥,每个条目都可以具有 TokenIntrospection
、UserInfo
或两者。它只会保留最大大小的条目数。
如果在添加新条目时缓存已满,则会尝试通过删除单个过期条目来查找空格。另外,如果激活,清理计时器会定期检查过期的条目并删除它们。
您可以使用默认缓存实现试验或注册自定义缓存。
在验证 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());
}
}
}
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());
}
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
考虑使用 quarkus.oidc.token.audience
属性来验证令牌 aud
(audience
)声明值。
单页应用(SPA)通常使用 XMLHttpRequest
(XHR)和 OIDC 提供程序提供的 JavaScript 实用程序代码,以获取 bearer 令牌来访问 Quarkus 服务
应用程序。
例如,如果使用 Keycloak,您可以使用 keycloak.js
验证用户并从 SPA 刷新过期的令牌:
<html>
<head>
<title>keycloak-spa</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="http://localhost:8180/js/keycloak.js"></script>
<script>
var keycloak = new Keycloak();
keycloak.init({onLoad: 'login-required'}).success(function () {
console.log('User is now authenticated.');
}).error(function () {
window.location.reload();
});
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();
});
});
}
</script>
</head>
<body>
<button onclick="makeAjaxRequest()">Request</button>
</body>
</html>
<html>
<head>
<title>keycloak-spa</title>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script src="http://localhost:8180/js/keycloak.js"></script>
<script>
var keycloak = new Keycloak();
keycloak.init({onLoad: 'login-required'}).success(function () {
console.log('User is now authenticated.');
}).error(function () {
window.location.reload();
});
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();
});
});
}
</script>
</head>
<body>
<button onclick="makeAjaxRequest()">Request</button>
</body>
</html>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果您计划从在不同域上运行的单页应用中使用 OIDC 服务
应用程序,您必须配置跨原始资源共享(CORS)。如需更多信息,请参阅"Cross-origin 资源共享"指南中的 CORS 过滤器 部分。
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
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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
有关 bearer 访问令牌传播到下游服务的详情,请参考 Quarkus "OpenID Connect (OIDC)和 OAuth2 客户端和过滤器参考" 指南中的 Token propagation 部分。
当需要对 OIDC 供应商的远程请求时,使用 quarkus.oidc.runtime.OidcProviderClient
。如果需要内省 Bearer 令牌,则 OidcProviderClient
必须向 OIDC 提供程序进行身份验证。有关支持的验证选项的更多信息,请参阅 Quarkus "OpenID Connect authorization code flow mechanism for protect web application" 指南中的 OIDC provider client authentication 部分。
您可以通过在测试项目中添加以下依赖项开始测试:
使用 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>
<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>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
使用 Gradle:
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")
testImplementation("io.rest-assured:rest-assured")
testImplementation("io.quarkus:quarkus-junit5")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
在您的测试项目中添加以下依赖项:
使用 Maven:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
使用 Gradle:
testImplementation("io.quarkus:quarkus-test-oidc-server")
testImplementation("io.quarkus:quarkus-test-oidc-server")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
准备 REST 测试端点并设置 application.properties
。例如:
keycloak.url is set by OidcWiremockTestResource
# keycloak.url is set by OidcWiremockTestResource
quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus/
quarkus.oidc.client-id=quarkus-service-app
quarkus.oidc.application-type=service
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
最后,编写测试代码。例如:
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();
}
}
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();
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
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"));
}
}
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果您使用 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
%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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
要启动,请添加相同的依赖项 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"));
}
}
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
此测试代码使用来自 test Auth0
域的 密码
授权获取令牌,该密码使用客户端 id test-auth0-client
注册了应用,并使用密码 alice
创建用户 alice
。要使测试正常工作,test Auth0
应用必须启用 密码
授权。这个示例代码还演示了如何传递额外的参数。对于 Auth0
,它们是 audience
和 scope
参数。
针对 Keycloak 进行集成测试的首选方法是 Keycloak 的 Dev Services 。用于 Keycloak 的 dev Services
将启动并初始化测试容器。然后,它将创建一个 quarkus
realm 和 quarkus-app
客户端(secret
secret),并添加 alice
(admin
和 user
roles)和 bob
(用户角色)用户,其中所有这些属性都可以自定义。
首先,添加以下依赖项,它提供实用程序类 io.quarkus.test.keycloak.client.KeycloakTestClient
,您可以用于测试获取访问令牌:
使用 Maven:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-keycloak-server</artifactId>
<scope>test</scope>
</dependency>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
使用 Gradle:
testImplementation("io.quarkus:quarkus-test-keycloak-server")
testImplementation("io.quarkus:quarkus-test-keycloak-server")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
接下来,准备 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
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果在运行测试前必须将自定义域文件导入到 Keycloak 中,请为 Keycloak 配置 Dev Services
,如下所示:
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.keycloak.devservices.realm-path=quarkus-realm.json
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
最后,编写您的测试,它将在 JVM 模式中执行,如下例所示:
有关为 Keycloak 初始化和配置 Dev Services 的更多信息,请参阅 Keycloak 的 Dev Services 指南。
您可以使用本地内联公钥来测试 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
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
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
要生成 JWT 令牌,请从 主
Quarkus 存储库中的 integration-tests/oidc-tenancy
复制 privateKey.pem
,并使用与上一 WireMock 部分中类似的测试代码。如果需要,您可以使用自己的测试密钥。
与 WireMock 方法相比,这种方法提供有限的覆盖范围。例如,不包括远程通信代码。
您可以使用 @TestSecurity
和 @OidcSecurity
注释来测试 服务
应用程序端点代码(取决于以下注入之一或全部三个):
JsonWebToken
UserInfo
OidcConfigurationMetadata
首先,添加以下依赖项:
使用 Maven:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-security-oidc</artifactId>
<scope>test</scope>
</dependency>
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
使用 Gradle:
testImplementation("io.quarkus:quarkus-test-security-oidc")
testImplementation("io.quarkus:quarkus-test-security-oidc")
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
按照以下示例中所述编写测试代码:
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.OidcConfigurationMetadata;
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"));
}
}
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.OidcConfigurationMetadata;
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"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
在这个代码示例中使用的 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");
}
}
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");
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
您必须始终使用 @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-claims-userinfo-metadata").then()
.body(is("userOidc:viewer:userOidc:viewer"));
}
}
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-claims-userinfo-metadata").then()
.body(is("userOidc:viewer:userOidc:viewer"));
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
在这个代码示例中使用的 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");
}
}
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");
}
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
@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 {
}
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.METHOD })
@TestSecurity(user = "userOidc", roles = "viewer")
@OidcSecurity(introspectionRequired = true,
introspection = {
@TokenIntrospection(key = "email", value = "user@gmail.com")
}
)
public @interface TestSecurityMetaAnnotation {
}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果多个测试方法必须使用同一组安全设置,这特别有用。
要查看有关令牌验证错误的更多详细信息,请启用 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
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcProvider".min-level=TRACE
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
要查看有关 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
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.runtime.OidcRecorder".min-level=TRACE
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
与自动发现或配置了 quarkus.oidc.auth-server-url
内部 URL 的 URL 相比,OIDC 供应商和其他端点可能具有不同的 HTTP (S) URL。例如,假设您的 SPA 从外部令牌端点地址获取令牌,并将其发送到 Quarkus 作为 bearer 令牌。在这种情况下,端点可能会报告签发者验证失败。
在这种情况下,如果您使用 Keycloak,使用 KEYCLOAK_FRONTEND_URL
系统属性将其设置为外部可访问的基本 URL。如果使用其他 OIDC 供应商,请参阅您的供应商文档。
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
Token audience claim must contain 'quarkus-app'
# Set client-id
quarkus.oidc.client-id=quarkus-app
# Token audience claim must contain 'quarkus-app'
quarkus.oidc.token.audience=${quarkus.oidc.client-id}
Copy to Clipboard
Copied!
Toggle word wrap
Toggle overflow
如果您设置了 quarkus.oidc.client-id
,但您的端点不需要远程访问 OIDC 供应商端点之一(整数、令牌获取等),请不要使用 quarkus.oidc.credentials
或类似属性设置客户端 secret,因为它不会被使用。
Quarkus web-app
应用程序总是需要 quarkus.oidc.client-id
属性。