OpenID Connect (OIDC) 和 OAuth2 客户端和过滤器
您可以使用 Quarkus 扩展来管理 OpenID Connect 和 OAuth 2.0 访问令牌,专注于获取、刷新和传播令牌。
这包括以下内容:
-
使用
quarkus-oidc-client
、quarkus-rest-client-oidc-filter
和quarkus-resteasy-client-oidc-filter
扩展,从符合 OpenID Connect 和 OAuth 2.0 的授权服务器(如 Keycloak)获取和刷新访问令牌。 -
使用
quarkus-rest-client-oidc-token-propagation
和quarkus-resteasy-client-oidc-token-propagation
扩展来传播当前的Bearer
或Authorization Code Flow
访问令牌。
这些扩展管理的访问令牌可以用作 HTTP Authorization Bearer 令牌来访问远程服务。
OidcClient
添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client</artifactId>
</dependency>
quarkus-oidc-client
扩展提供了一个响应式的 io.quarkus.oidc.client.OidcClient
,可用于使用 SmallRye Mutiny 的 Uni
和 Vert.x WebClient
来获取和刷新令牌。
OidcClient
在构建时使用 IDP 令牌端点 URL 进行初始化,该 URL 可以自动发现或手动配置。OidcClient
使用此端点通过 client_credentials
或 password
等令牌授予来获取访问令牌,并通过 refresh_token
授予来刷新令牌。
令牌端点配置
默认情况下,令牌端点地址是通过将 /.well-known/openid-configuration
路径添加到已配置的 quarkus.oidc-client.auth-server-url
来发现的。
例如,给定这个 Keycloak URL:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus
OidcClient
将会发现令牌端点 URL 是 https://:8180/auth/realms/quarkus/protocol/openid-connect/tokens
。
或者,如果发现端点不可用,或者您想节省发现端点的往返次数,您可以禁用发现并使用相对路径值配置令牌端点地址。例如:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus
quarkus.oidc-client.discovery-enabled=false
# Token endpoint: https://:8180/auth/realms/quarkus/protocol/openid-connect/tokens
quarkus.oidc-client.token-path=/protocol/openid-connect/tokens
配置令牌端点 URL 的一个更简洁的方式,无需发现,只需将 quarkus.oidc-client.token-path
设置为绝对 URL:
quarkus.oidc-client.token-path=https://:8180/auth/realms/quarkus/protocol/openid-connect/tokens
在这种情况下,不需要设置 quarkus.oidc-client.auth-server-url
和 quarkus.oidc-client.discovery-enabled
。
支持的令牌授予
OidcClient
可以用来获取令牌的主要令牌授予是 client_credentials
(默认)和 password
授予。
客户端凭据授予
以下是配置 OidcClient
以使用 client_credentials
授予的方法:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
client_credentials
授予允许通过使用 quarkus.oidc-client.grant-options.client.<param-name>=<value>
为令牌请求设置额外的参数。以下是如何通过使用 audience
参数设置目标令牌接收者:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
# 'client' is a shortcut for `client_credentials`
quarkus.oidc-client.grant.type=client
quarkus.oidc-client.grant-options.client.audience=https://example.com/api
密码授予
以下是配置 OidcClient
以使用 password
授予的方法:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice
与客户端凭据授予的自定义方式类似,可以使用 quarkus.oidc-client.grant-options.password
配置前缀进一步自定义。
其他授予
OidcClient
还可以通过使用需要无法在配置中捕获的额外输入参数的授予来帮助获取令牌。这些授予是 refresh_token
(带有外部刷新令牌)、authorization_code
,以及可用于交换当前访问令牌的两个授予,即 urn:ietf:params:oauth:grant-type:token-exchange
和 urn:ietf:params:oauth:grant-type:jwt-bearer
。
如果您需要获取访问令牌,并且已将现有刷新令牌发布到当前的 Quarkus 端点,则必须使用 refresh_token
授予。此授予使用带外刷新令牌来获取新的令牌集。在这种情况下,请按如下方式配置 OidcClient
:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=refresh
然后,您可以使用 OidcClient.refreshTokens
方法以及提供的刷新令牌来获取访问令牌。
如果您正在构建复杂的微服务应用程序,并希望避免相同的 Bearer
令牌被传播到多个服务或被多个服务使用,则可能需要使用 urn:ietf:params:oauth:grant-type:token-exchange
或 urn:ietf:params:oauth:grant-type:jwt-bearer
授予。有关更多详细信息,请参阅 Quarkus REST 的令牌传播 和 RESTEasy Classic 的令牌传播。
如果您由于某种原因无法使用 Quarkus OIDC 扩展来支持授权码流,则可能需要使用 OidcClient
来支持 authorization code
授予。如果您有充分的理由实现授权码流,那么您可以按如下方式配置 OidcClient
:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=code
然后,您可以使用 OidcClient.accessTokens
方法接受额外的属性 Map,并传递当前的 code
和 redirect_uri
参数来用授权码交换令牌。
OidcClient
还支持 urn:openid:params:grant-type:ciba
授予。
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=ciba
然后,您可以使用 OidcClient.accessTokens
方法接受额外的属性 Map,并传递 auth_req_id
参数来交换令牌授权码。
直接使用 OidcClient
您可以直接使用 OidcClient
获取访问令牌,并将其设置为 HTTP Authorization
标头中的 Bearer
方案值。
例如,假设 Quarkus 端点需要访问一个返回用户名的微服务。首先,创建一个 REST 客户端:
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.HeaderParam;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@RegisterRestClient
@Path("/")
public interface RestClientWithTokenHeaderParam {
@GET
@Produces("text/plain")
@Path("userName")
Uni<String> getUserName(@HeaderParam("Authorization") String authorization);
}
现在,使用 OidcClient
获取令牌并传播它们:
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import io.quarkus.oidc.client.runtime.TokensHelper;
import io.quarkus.oidc.client.OidcClient;
import io.smallrye.mutiny.Uni;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
@Path("/service")
public class OidcClientResource {
@Inject
OidcClient client;
TokensHelper tokenHelper = new TokensHelper(); (1)
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient;
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
return tokenHelper.getTokens(client).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
1 | io.quarkus.oidc.client.runtime.TokensHelper 负责管理访问令牌的获取和刷新。 |
注入令牌
您可以注入在内部使用 OidcClient
的 Tokens
。Tokens
可用于获取访问令牌并在必要时刷新它们。
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import io.quarkus.oidc.client.Tokens;
@Path("/service")
public class OidcClientResource {
@Inject Tokens tokens;
@GET
public String getResponse() {
// Get the access token, which might have been refreshed.
String accessToken = tokens.getAccessToken();
// Use the access token to configure MP RestClient Authorization header/etc
}
}
使用 OidcClients
io.quarkus.oidc.client.OidcClients
是 OidcClient
的容器,它包括一个默认的 OidcClient
和命名的客户端,可以这样配置:
quarkus.oidc-client.client-enabled=false
quarkus.oidc-client.jwt-secret.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.jwt-secret.client-id=quarkus-app
quarkus.oidc-client.jwt-secret.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
在这种情况下,默认客户端被 client-enabled=false
属性禁用。jwt-secret
客户端可以这样访问:
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.runtime.TokensHelper;
@Path("/clients")
public class OidcClientResource {
@Inject
OidcClients clients;
TokensHelper tokenHelper = new TokensHelper();
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient; (1)
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
OidcClient client = clients.getClient("jwt-secret");
return tokenHelper.getTokens(client).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
1 | 请参阅 直接使用 OidcClient 部分中的 RestClientWithTokenHeaderParam 声明。 |
如果您还使用 OIDC 多租户,并且每个 OIDC 租户都有其关联的
|
您也可以通过编程方式创建新的 OidcClient
。例如,假设您必须在启动时创建它:
package org.acme.security.openid.connect.client;
import java.util.Map;
import org.eclipse.microprofile.config.inject.ConfigProperty;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.runtime.OidcClientConfig;
import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.runtime.StartupEvent;
import io.smallrye.mutiny.Uni;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.event.Observes;
import jakarta.inject.Inject;
@ApplicationScoped
public class OidcClientCreator {
@Inject
OidcClients oidcClients;
@ConfigProperty(name = "quarkus.oidc.auth-server-url")
String oidcProviderAddress;
private volatile OidcClient oidcClient;
public void startup(@Observes StartupEvent event) {
createOidcClient().subscribe().with(client -> {oidcClient = client;});
}
public OidcClient getOidcClient() {
return oidcClient;
}
private Uni<OidcClient> createOidcClient() {
OidcClientConfig cfg = OidcClientConfig
.authServerUrl(oidcProviderAddress)
.id("myclient")
.clientId("backend-service")
.credentials("secret")
.grant(Type.PASSWORD)
.grantOptions("password", Map.of("username", "alice", "password", "alice"))
.build();
return oidcClients.newClient(cfg);
}
}
现在,您可以这样使用此客户端:
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.runtime.TokensHelper;
@Path("/clients")
public class OidcClientResource {
@Inject
OidcClientCreator oidcClientCreator;
TokensHelper tokenHelper = new TokensHelper();
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient; (1)
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
1 | 请参阅 直接使用 OidcClient 部分中的 RestClientWithTokenHeaderParam 声明。 |
注入命名的 OidcClient 和令牌
在有多个配置的 OidcClient
对象的情况下,您可以通过额外的限定符 @NamedOidcClient
来指定 OidcClient
注入目标,而不是使用 OidcClients
。
package org.acme.security.openid.connect.client;
import org.eclipse.microprofile.rest.client.inject.RestClient;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.runtime.TokensHelper;
@Path("/clients")
public class OidcClientResource {
@Inject
@NamedOidcClient("jwt-secret")
OidcClient client;
TokensHelper tokenHelper = new TokensHelper();
@Inject
@RestClient
RestClientWithTokenHeaderParam restClient; (1)
@GET
@Path("user-name")
@Produces("text/plain")
public Uni<String> getUserName() {
return tokenHelper.getTokens(client).onItem()
.transformToUni(tokens -> restClient.getUserName("Bearer " + tokens.getAccessToken()));
}
}
1 | 请参阅 直接使用 OidcClient 部分中的 RestClientWithTokenHeaderParam 声明。 |
同样的限定符也可用于指定用于 Tokens
注入的 OidcClient
。
import java.io.IOException;
import jakarta.annotation.Priority;
import jakarta.enterprise.context.RequestScoped;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.oidc.client.NamedOidcClient;
import io.quarkus.oidc.client.Tokens;
@Provider
@Priority(Priorities.AUTHENTICATION)
@RequestScoped
public class OidcClientRequestCustomFilter implements ClientRequestFilter {
@Inject
@NamedOidcClient("jwt-secret")
Tokens tokens;
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
}
}
在 RestClient Reactive ClientFilter 中使用 OidcClient
添加以下 Maven 依赖:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-rest-client-oidc-filter</artifactId>
</dependency>
这将也会引入 io.quarkus:quarkus-oidc-client 。 |
quarkus-rest-client-oidc-filter
扩展提供了 io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter
。
它的工作方式与 OidcClientRequestFilter
类似(请参阅 在 MicroProfile RestClient 客户端过滤器中使用 OidcClient)——它使用 OidcClient
获取访问令牌,在需要时刷新它,并将其设置为 HTTP Authorization Bearer
方案值。区别在于它与 Reactive RestClient 一起工作,并实现了一个非阻塞的客户端过滤器,在获取或刷新令牌时不会阻塞当前的 IO 线程。
OidcClientRequestReactiveFilter
会延迟初始令牌的获取,直到它被执行,以避免阻塞 IO 线程。
您可以通过使用 io.quarkus.oidc.client.reactive.filter.OidcClientFilter
或 org.eclipse.microprofile.rest.client.annotation.RegisterProvider
注解来选择性地注册 OidcClientRequestReactiveFilter
。
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
或
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
OidcClientRequestReactiveFilter
默认使用一个默认的 OidcClient
。可以通过 quarkus.rest-client-oidc-filter.client-name
配置属性选择一个命名的 OidcClient
。您还可以通过设置 @OidcClientFilter
注解的 value
属性来选择 OidcClient
。通过注解设置的客户端名称比 quarkus.rest-client-oidc-filter.client-name
配置属性具有更高的优先级。例如,给定 这个 jwt-secret
命名的 OIDC 客户端声明,您可以这样引用此客户端:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
Uni<String> getUserName();
}
在 RestClient ClientFilter 中使用 OidcClient
添加以下 Maven 依赖:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-resteasy-client-oidc-filter</artifactId>
</dependency>
这将也会引入 io.quarkus:quarkus-oidc-client 。 |
quarkus-resteasy-client-oidc-filter
扩展提供了 io.quarkus.oidc.client.filter.OidcClientRequestFilter
Jakarta REST ClientRequestFilter,它使用 OidcClient
来获取访问令牌,在需要时刷新它,并将其设置为 HTTP Authorization Bearer
方案值。
默认情况下,此过滤器将在其初始化时获取第一个访问令牌和刷新令牌对。如果访问令牌的有效期很短且没有刷新令牌,则应通过 quarkus.oidc-client.early-tokens-acquisition=false
来延迟令牌的获取。
您可以通过使用 io.quarkus.oidc.client.filter.OidcClientFilter
或 org.eclipse.microprofile.rest.client.annotation.RegisterProvider
注解来选择性地注册 OidcClientRequestFilter
。
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
或
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(OidcClientRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
或者,如果将 quarkus.resteasy-client-oidc-filter.register-filter=true
属性设置为 true
,则可以自动将 OidcClientRequestFilter
注册到所有 MP Rest 或 Jakarta REST 客户端。
OidcClientRequestFilter
默认使用一个默认的 OidcClient
。可以通过 quarkus.resteasy-client-oidc-filter.client-name
配置属性选择一个命名的 OidcClient
。您还可以通过设置 @OidcClientFilter
注解的 value
属性来选择 OidcClient
。通过注解设置的客户端名称比 quarkus.resteasy-client-oidc-filter.client-name
配置属性具有更高的优先级。例如,给定 这个 jwt-secret
命名的 OIDC 客户端声明,您可以这样引用此客户端:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
使用自定义 RestClient ClientFilter
如果需要,您可以使用自己的自定义过滤器并注入 Tokens
。
import java.io.IOException;
import jakarta.annotation.Priority;
import jakarta.inject.Inject;
import jakarta.ws.rs.Priorities;
import jakarta.ws.rs.client.ClientRequestContext;
import jakarta.ws.rs.client.ClientRequestFilter;
import jakarta.ws.rs.core.HttpHeaders;
import jakarta.ws.rs.ext.Provider;
import io.quarkus.oidc.client.Tokens;
@Provider
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter implements ClientRequestFilter {
@Inject
Tokens tokens;
@Override
public void filter(ClientRequestContext requestContext) throws IOException {
requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
}
}
Tokens
生产者将获取并刷新令牌,而自定义过滤器将决定如何以及何时使用令牌。
您也可以注入命名的 Tokens
,请参阅 注入命名的 OidcClient 和令牌。
刷新访问令牌
OidcClientRequestReactiveFilter
、OidcClientRequestFilter
和 Tokens
生产者将在刷新令牌可用时刷新当前过期的访问令牌。此外,quarkus.oidc-client.refresh-token-time-skew
属性可用于抢先刷新访问令牌,以避免发送接近过期的访问令牌,这可能会导致 HTTP 401 错误。例如,如果此属性设置为 3S
,并且访问令牌将在 3 秒内过期,则该令牌将被自动刷新。
如果需要刷新访问令牌,但没有可用的刷新令牌,则会尝试通过使用已配置的授予(如 client_credentials
)来获取新令牌。
某些 OpenID Connect 提供商在 client_credentials
授予响应中不会返回刷新令牌。例如,从 Keycloak 12 开始,默认情况下不会为 client_credentials
返回刷新令牌。提供商也可能限制刷新令牌的使用次数。
撤销访问令牌
如果您的 OpenID Connect 提供商(如 Keycloak)支持令牌撤销端点,则可以使用 OidcClient#revokeAccessToken
来撤销当前访问令牌。撤销端点 URL 将与令牌请求 URI 一起发现,或者可以使用 quarkus.oidc-client.revoke-path
进行配置。
您可能希望在通过 REST 客户端使用此令牌失败并出现 HTTP 401
状态码时,或者在访问令牌已使用很长时间并且您想刷新它时,撤销访问令牌。
这可以通过请求使用刷新令牌来刷新访问令牌来实现。但是,如果刷新令牌不可用,则可以通过先撤销它,然后请求新的访问令牌来刷新它。
OidcClient 身份验证
OidcClient
必须向 OpenID Connect Provider 进行身份验证,以便 client_credentials
和其他授予请求能够成功。所有 OIDC 客户端身份验证 选项都受到支持,例如:
client_secret_basic
:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=mysecret
或
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.client-secret.value=mysecret
或者使用从 CredentialsProvider 获取的 secret:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
# This key is used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.client-secret.provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.client-secret.provider.name=oidc-credentials-provider
client_secret_post
:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.client-secret.value=mysecret
quarkus.oidc-client.credentials.client-secret.method=post
client_secret_jwt
,签名算法为 HS256
。
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow
或者使用从 CredentialsProvider 获取的 secret,签名算法为 HS256
。
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
# This is a key that will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key
# This is the keyring provided to the CredentialsProvider when looking up the secret, set only if required by the CredentialsProvider implementation
quarkus.oidc.credentials.client-secret.provider.keyring-name=oidc
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider
private_key_jwt
,PEM 密钥内联在 application.properties 中,签名算法为 RS256
。
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key=Base64-encoded private key representation
private_key_jwt
,使用 PEM 密钥文件,签名算法为 RS256
。
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-file=privateKey.pem
private_key_jwt
,使用密钥库文件,签名算法为 RS256
。
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-store-file=keystore.pkcs12
quarkus.oidc-client.credentials.jwt.key-store-password=mypassword
quarkus.oidc-client.credentials.jwt.key-password=mykeypassword
# Private key alias inside the keystore
quarkus.oidc-client.credentials.jwt.key-id=mykeyAlias
使用 client_secret_jwt
或 private_key_jwt
身份验证方法可以确保客户端 secret 不会在网络上传输。
其他 JWT 身份验证选项
如果使用 client_secret_jwt
或 private_key_jwt
身份验证方法,则可以自定义 JWT 签名算法、密钥标识符、受众、主题和发行者,例如:
# private_key_jwt client authentication
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-file=privateKey.pem
# This is a token key identifier 'kid' header - set it if your OpenID Connect provider requires it.
# Note that if the key is represented in a JSON Web Key (JWK) format with a `kid` property, then
# using 'quarkus.oidc-client.credentials.jwt.token-key-id' is unnecessary.
quarkus.oidc-client.credentials.jwt.token-key-id=mykey
# Use the RS512 signature algorithm instead of the default RS256
quarkus.oidc-client.credentials.jwt.signature-algorithm=RS512
# The token endpoint URL is the default audience value; use the base address URL instead:
quarkus.oidc-client.credentials.jwt.audience=${quarkus.oidc-client.auth-server-url}
# custom subject instead of the client ID:
quarkus.oidc-client.credentials.jwt.subject=custom-subject
# custom issuer instead of the client ID:
quarkus.oidc-client.credentials.jwt.issuer=custom-issuer
JWT Bearer
RFC7523 解释了如何使用 JWT Bearer 令牌来对客户端进行身份验证,有关更多信息,请参阅 使用 JWT 进行客户端身份验证 部分。
可以按如下方式启用它:
quarkus.oidc-client.auth-server-url=${auth-server-url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.source=bearer
接下来,必须将 JWT bearer 令牌作为 client_assertion
参数提供给 OIDC 客户端。
Quarkus 可以从文件系统加载 JWT bearer 令牌。例如,在 Kubernetes 中,服务帐户令牌投影可以挂载到 /var/run/secrets/tokens
路径。然后,您只需配置 JWT bearer 令牌路径,如下所示:
quarkus.oidc-client.credentials.jwt.token-path=/var/run/secrets/tokens (1)
1 | JWT bearer 令牌的路径。Quarkus 从文件系统加载新令牌,并在令牌过期时重新加载它。 |
您的另一个选择是使用 OidcClient
方法来获取或刷新令牌,这些方法接受额外的授予参数,例如 oidcClient.getTokens(Map.of("client_assertion", "ey…"))
。
如果您使用 OIDC 客户端过滤器,则必须注册一个自定义过滤器来提供此断言。
以下是 Quarkus REST(以前称为 RESTEasy Reactive)自定义过滤器的示例:
package io.quarkus.it.keycloak;
import java.util.Map;
import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestReactiveFilter {
@Override
protected Map<String, String> additionalParameters() {
return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
}
}
以下是 RESTEasy Classic 自定义过滤器的示例:
package io.quarkus.it.keycloak;
import java.util.Map;
import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter;
import io.quarkus.oidc.common.runtime.OidcConstants;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestFilter {
@Override
protected Map<String, String> additionalParameters() {
return Map.of(OidcConstants.CLIENT_ASSERTION, "ey...");
}
}
Apple POST JWT
Apple OpenID Connect 提供商使用 client_secret_post
方法,其中 secret 是使用 private_key_jwt
身份验证方法生成的 JWT,但具有 Apple 帐户特定的发行者和主题属性。
quarkus-oidc-client
支持非标准的 client_secret_post_jwt
身份验证方法,可以按如下方式配置:
quarkus.oidc-client.auth-server-url=${apple.url}
quarkus.oidc-client.client-id=${apple.client-id}
quarkus.oidc-client.credentials.client-secret.method=post-jwt
quarkus.oidc-client.credentials.jwt.key-file=ecPrivateKey.pem
quarkus.oidc-client.credentials.jwt.signature-algorithm=ES256
quarkus.oidc-client.credentials.jwt.subject=${apple.subject}
quarkus.oidc-client.credentials.jwt.issuer=${apple.issuer}
Mutual TLS
某些 OpenID Connect 提供商要求客户端在 mutual TLS (mTLS
) 身份验证过程中进行身份验证。
可以按如下方式配置 quarkus-oidc-client
以支持 mTLS
:
quarkus.oidc-client.tls.tls-configuration-name=oidc-client
# configure hostname verification if necessary
#quarkus.tls.oidc-client.hostname-verification-algorithm=NONE
# Keystore configuration
quarkus.tls.oidc-client.key-store.p12.path=client-keystore.p12
quarkus.tls.oidc-client.key-store.p12.password=${key-store-password}
# Add more keystore properties if needed:
#quarkus.tls.oidc-client.key-store.p12.alias=keyAlias
#quarkus.tls.oidc-client.key-store.p12.alias-password=keyAliasPassword
# Truststore configuration
quarkus.tls.oidc-client.trust-store.p12.path=client-truststore.p12
quarkus.tls.oidc-client.trust-store.p12.password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.tls.oidc-client.trust-store.p12.alias=certAlias
OIDC Client SPI
当您的自定义扩展必须使用 OIDC 客户端支持的 OIDC 令牌授予之一来获取 OIDC 令牌时,此扩展可以仅依赖 OIDC Client SPI,并让 OIDC 客户端本身根据需要获取和刷新访问令牌。
添加以下依赖项
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-oidc-client-spi</artifactId>
</dependency>
接下来,更新您的扩展以使用所需的 io.quarkus.oidc.client.spi.TokenProvider
CDI bean,例如:
package org.acme.extension;
import jakarta.inject.Inject;
import io.quarkus.oidc.client.spi.TokenProvider;
public class ExtensionOAuth2Support {
@Inject
TokenProvider tokenProvider;
public Uni<String> getAccessToken() {
return tokenProvider.getAccessToken();
}
}
目前,io.quarkus.oidc.client.spi.TokenProvider
仅适用于默认的 OIDC 客户端,因为自定义扩展不太可能感知多个命名的 OIDC 客户端。
测试
首先将以下依赖项添加到您的测试项目中
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<scope>test</scope>
</dependency>
Wiremock
将以下依赖项添加到您的测试项目中:
<dependency>
<groupId>org.wiremock</groupId>
<artifactId>wiremock</artifactId>
<scope>test</scope>
<version>${wiremock.version}</version> (1)
</dependency>
1 | 使用正确的 Wiremock 版本。所有可用版本都可以在 这里 找到。 |
编写一个基于 Wiremock 的 QuarkusTestResourceLifecycleManager
,例如:
package io.quarkus.it.keycloak;
import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;
import java.util.HashMap;
import java.util.Map;
import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.Options.ChunkedEncodingPolicy;
import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;
public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager {
private WireMockServer server;
@Override
public Map<String, String> start() {
server = new WireMockServer(wireMockConfig().dynamicPort().useChunkedTransferEncoding(ChunkedEncodingPolicy.NEVER));
server.start();
server.stubFor(WireMock.post("/tokens")
.withRequestBody(matching("grant_type=password&username=alice&password=alice"))
.willReturn(WireMock
.aResponse()
.withHeader("Content-Type", "application/json")
.withBody(
"{\"access_token\":\"access_token_1\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}")));
server.stubFor(WireMock.post("/tokens")
.withRequestBody(matching("grant_type=refresh_token&refresh_token=refresh_token_1"))
.willReturn(WireMock
.aResponse()
.withHeader("Content-Type", "application/json")
.withBody(
"{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}")));
Map<String, String> conf = new HashMap<>();
conf.put("keycloak.url", server.baseUrl());
return conf;
}
@Override
public synchronized void stop() {
if (server != null) {
server.stop();
server = null;
}
}
}
准备 REST 测试端点。您可以有一个测试前端端点,该端点使用已注册 OidcClient 过滤器的注入的 MP REST 客户端来调用下游端点。此端点会将其接收到的令牌回显。例如,请参阅 Quarkus 主仓库中的 integration-tests/oidc-client-wiremock
。
设置 application.properties
,例如:
# Use the 'keycloak.url' property set by the test KeycloakRealmResourceManager
quarkus.oidc-client.auth-server-url=${keycloak.url:replaced-by-test-resource}
quarkus.oidc-client.discovery-enabled=false
quarkus.oidc-client.token-path=/tokens
quarkus.oidc-client.client-id=quarkus-service-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice
最后,编写测试代码。给定上述基于 Wiremock 的资源,第一次测试调用应返回 access_token_1
访问令牌,该令牌将在 4 秒后过期。使用 awaitility
等待大约 5 秒钟,然后下一次测试调用应返回 access_token_2
访问令牌,这证实了已过期的 access_token_1
访问令牌已被刷新。
Keycloak
如果您使用 Keycloak,可以使用在 OpenID Connect Bearer Token 集成测试 Keycloak 部分中描述的相同方法。
如何检查日志中的错误
启用 io.quarkus.oidc.client.runtime.OidcClientImpl
的 TRACE
级别日志记录,以查看有关令牌获取和刷新错误的更多详细信息。
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE
启用 io.quarkus.oidc.client.runtime.OidcClientRecorder
的 TRACE
级别日志记录,以查看有关 OidcClient 初始化错误的更多详细信息。
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-level=TRACE
OIDC 请求过滤器
您可以通过注册一个或多个 OidcRequestFilter
实现来过滤 OIDC 客户端向 OIDC 提供商发出的 OIDC 请求,这些实现可以更新或添加新的请求标头,或分析请求正文。
您可以有一个过滤器拦截对所有 OIDC 提供商端点的请求,或者使用 @OidcEndpoint
注解将此过滤器应用于仅对特定端点的请求。例如:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcEndpoint;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.core.http.HttpMethod;
@ApplicationScoped
@OidcEndpoint(value = Type.TOKEN)
@Unremovable
public class OidcRequestCustomizer implements OidcRequestFilter {
@Override
public void filter(OidcRequestContext requestContext) {
HttpMethod method = requestContext.request().method();
String uri = requestContext.request().uri();
if (method == HttpMethod.POST && uri.endsWith("/token") && requestContext.requestBody() != null) {
requestContext.request().putHeader("Digest", calculateDigest(requestContext.requestBody().toString()));
}
}
private String calculateDigest(String bodyString) {
// Apply the required digest algorithm to the body string
}
}
OidcRequestContextProperties
可用于访问请求属性。目前,您可以使用 client_id
键来访问客户端租户 ID,使用 grant_type
键来访问 OIDC 客户端用于获取令牌的授予类型。
OIDC 响应过滤器
您可以通过注册一个或多个 OidcResponseFilter
实现来过滤对 OIDC 客户端请求的响应,这些实现可以检查响应状态、标头和正文,以便记录它们或执行其他操作。
您可以有一个过滤器拦截对所有 OIDC 客户端请求的响应,或者使用 @OidcEndpoint
注解将此过滤器应用于仅对特定 OIDC 客户端请求的响应。例如:
package io.quarkus.it.keycloak;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
import io.quarkus.arc.Unremovable;
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;
@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN) (1)
public class TokenEndpointResponseFilter implements OidcResponseFilter {
private static final Logger LOG = Logger.getLogger(TokenEndpointResponseFilter.class);
@Override
public void filter(OidcResponseContext rc) {
String contentType = rc.responseHeaders().get("Content-Type"); (2)
if (contentType.equals("application/json")
&& "refresh_token".equals(rc.requestProperties().get(OidcConstants.GRANT_TYPE)) (3)
&& rc.responseBody().toJsonObject().containsKey("refresh_token")) { (4)
LOG.debug("Tokens have been refreshed");
}
}
}
1 | 将此过滤器限制为仅针对 OIDC 令牌端点的请求。 |
2 | 检查响应 Content-Type 标头。 |
3 | 使用 OidcRequestContextProperties 请求属性来确认这是 refresh_grant 令牌授予的响应。 |
4 | 确认响应 JSON 包含 refresh_token 属性。 |
Quarkus REST 的令牌传播
quarkus-rest-client-oidc-token-propagation
扩展提供了一个 REST 客户端过滤器 io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter
,它简化了身份验证信息的传播。此客户端将当前活动请求中存在的 bearer 令牌 或从 authorization code flow 机制获取的令牌作为 HTTP Authorization
标头中的 Bearer
方案值进行传播。
您可以通过使用 io.quarkus.oidc.token.propagation.common.AccessToken
或 org.eclipse.microprofile.rest.client.annotation.RegisterProvider
注解来选择性地注册 AccessTokenRequestReactiveFilter
,例如:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
或
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
此外,AccessTokenRequestReactiveFilter
可以支持需要先交换令牌再进行传播的复杂应用程序。
如果您使用 Keycloak 或其他支持 Token Exchange 令牌授予的 OIDC 提供商,则可以按如下方式配置 AccessTokenRequestReactiveFilter
来交换令牌:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
quarkus.rest-client-oidc-token-propagation.exchange-token=true (1)
1 | 请注意,当 OidcClient 名称通过 io.quarkus.oidc.token.propagation.common.AccessToken#exchangeTokenClient 注解属性设置时,exchange-token 配置属性将被忽略。 |
AccessTokenRequestReactiveFilter 将使用 OidcClient 来交换当前令牌,您可以使用 quarkus.oidc-client.grant-options.exchange 来设置您的 OpenID Connect 提供商期望的额外交换属性。 |
如果您使用 Azure
等提供商,它们 要求使用 JWT bearer token grant 来交换当前令牌,则可以按如下方式配置 AccessTokenRequestReactiveFilter
来交换令牌:
quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
AccessTokenRequestReactiveFilter
默认使用一个默认的 OidcClient
。可以通过 quarkus.rest-client-oidc-token-propagation.client-name
配置属性或 io.quarkus.oidc.token.propagation.common.AccessToken#exchangeTokenClient
注解属性来选择一个命名的 OidcClient
。
RESTEasy Classic 的令牌传播
quarkus-resteasy-client-oidc-token-propagation
扩展提供了两个 Jakarta REST jakarta.ws.rs.client.ClientRequestFilter
类实现,它们简化了身份验证信息的传播。io.quarkus.oidc.token.propagation.AccessTokenRequestFilter
将当前活动请求中存在的 Bearer 令牌 或从 Authorization code flow 机制获取的令牌作为 HTTP Authorization
标头中的 Bearer
方案值进行传播。io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter
提供相同的功能,但此外还支持 JWT 令牌。
当您需要传播当前的 Authorization Code Flow 访问令牌时,即时令牌传播将工作得很好——因为代码流访问令牌(与 ID 令牌相对)旨在被传播,以便当前的 Quarkus 端点可以代表当前已进行身份验证的用户来访问远程服务。
但是,应避免直接的端到端 Bearer 令牌传播。例如,Client → Service A → Service B
,其中 Service B
接收由 Client
发送到 Service A
的令牌。在这种情况下,Service B
无法区分令牌是来自 Service A
还是直接来自 Client
。为了让 Service B
验证令牌来自 Service A
,它应该能够断言新的发行者和受众声明。
此外,复杂的应用程序可能需要先交换或更新令牌,然后再进行传播。例如,当 Service A
访问 Service B
时,访问上下文可能不同。在这种情况下,Service A
可能被授予更窄或完全不同的范围集来访问 Service B
。
以下各节将展示 AccessTokenRequestFilter
和 JsonWebTokenRequestFilter
如何提供帮助。
RestClient AccessTokenRequestFilter
AccessTokenRequestFilter
将所有令牌视为字符串,因此它可以处理 JWT 和不透明令牌。
您可以通过使用 io.quarkus.oidc.token.propagation.common.AccessToken
或 org.eclipse.microprofile.rest.client.annotation.RegisterProvider
注解来选择性地注册 AccessTokenRequestFilter
,例如:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.common.AccessToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
或
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(AccessTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
或者,如果将 quarkus.resteasy-client-oidc-token-propagation.register-filter
属性设置为 true
并且 quarkus.resteasy-client-oidc-token-propagation.json-web-token
属性设置为 false
(这是默认值),则可以自动将 AccessTokenRequestFilter
注册到所有 MP REST 或 Jakarta REST 客户端。
传播前交换令牌
如果需要在传播前交换当前访问令牌,并且您使用的是 Keycloak 或其他支持 Token Exchange 令牌授予的 OpenID Connect 提供商,则可以按如下方式配置 AccessTokenRequestFilter
:
quarkus.oidc-client.auth-server-url=https://:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
如果您使用 Azure
等提供商,它们 要求使用 JWT bearer token grant 来交换当前令牌,则可以按如下方式配置 AccessTokenRequestFilter
来交换令牌:
quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access
quarkus.resteasy-client-oidc-token-propagation.exchange-token=true
AccessTokenRequestFilter 将使用 OidcClient 来交换当前令牌,您可以使用 quarkus.oidc-client.grant-options.exchange 来设置您的 OpenID Connect 提供商期望的额外交换属性。 |
AccessTokenRequestFilter
默认使用一个默认的 OidcClient
。可以通过 quarkus.resteasy-client-oidc-token-propagation.client-name
配置属性来选择一个命名的 OidcClient
。
RestClient JsonWebTokenRequestFilter
如果您使用 Bearer JWT 令牌,并且这些令牌的声明(如 issuer
和 audience
)可以被修改并重新签名,则建议使用 JsonWebTokenRequestFilter
。它需要注入 org.eclipse.microprofile.jwt.JsonWebToken
,因此无法处理不透明令牌。此外,如果您的 OpenID Connect 提供商支持令牌交换协议,则建议改用 AccessTokenRequestFilter
——因为 AccessTokenRequestFilter
可以安全地交换 JWT 和不透明的 bearer 令牌。
JsonWebTokenRequestFilter
使 Service A
实现能够轻松地使用新的 issuer
和 audience
声明值更新注入的 org.eclipse.microprofile.jwt.JsonWebToken
,并使用新的签名再次保护更新后的令牌。唯一困难的步骤是确保 Service A
拥有签名密钥,该密钥应从安全的文件系统或远程安全存储(如 Vault)提供。
您可以通过使用 io.quarkus.oidc.token.propagation.JsonWebToken
或 org.eclipse.microprofile.rest.client.annotation.RegisterProvider
注解来选择性地注册 JsonWebTokenRequestFilter
,例如:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebToken;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@JsonWebToken
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
或
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
@RegisterRestClient
@RegisterProvider(JsonWebTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {
@GET
String getUserName();
}
或者,如果将 quarkus.resteasy-client-oidc-token-propagation.register-filter
和 quarkus.resteasy-client-oidc-token-propagation.json-web-token
属性都设置为 true
,则可以自动将 JsonWebTokenRequestFilter
注册到所有 MicroProfile REST 或 Jakarta REST 客户端。
传播前更新令牌
如果需要更新注入的令牌的 iss
(发行者)或 aud
(受众)声明并使用新签名再次保护,则可以按如下方式配置 JsonWebTokenRequestFilter
:
quarkus.resteasy-client-oidc-token-propagation.secure-json-web-token=true
smallrye.jwt.sign.key.location=/privateKey.pem
# Set a new issuer
smallrye.jwt.new-token.issuer=http://frontend-resource
# Set a new audience
smallrye.jwt.new-token.audience=http://downstream-resource
# Override the existing token issuer and audience claims if they are already set
smallrye.jwt.new-token.override-matching-claims=true
如前所述,如果您使用 Keycloak 或支持令牌交换协议的 OpenID Connect 提供商,请使用 AccessTokenRequestFilter
。
测试
通常,您需要准备两个 REST 测试端点。第一个端点使用注入的 MP REST 客户端和已注册的令牌传播过滤器来调用第二个端点。
要了解如何执行此操作,请遵循 OpenID Connect 客户端和令牌传播 快速入门,特别是其 测试 部分。
GraphQL 客户端集成
quarkus-oidc-client-graphql
扩展提供了一种将 OIDC 客户端集成到 GraphQL 客户端 中的方法,该方法与 REST 客户端的方法类似。当此扩展激活时,通过属性配置的任何 GraphQL 客户端(而非通过生成器以编程方式配置)将使用 OIDC 客户端获取访问令牌,然后将其设置为 Authorization
标头值。OIDC 客户端还将刷新过期的访问令牌。
要配置 GraphQL 客户端使用的 OIDC 客户端,请使用 quarkus.oidc-client-graphql.client-name
属性选择一个已配置的 OIDC 客户端,例如:
quarkus.oidc-client-graphql.client-name=oidc-client-for-graphql # example declaration of the OIDC client itself quarkus.oidc-client.oidc-client-for-graphql.auth-server-url=${keycloak.url} quarkus.oidc-client.oidc-client-for-graphql.grant.type=password quarkus.oidc-client.oidc-client-for-graphql.grant-options.password.username=${username} quarkus.oidc-client.oidc-client-for-graphql.grant-options.password.password=${password} quarkus.oidc-client.oidc-client-for-graphql.client-id=${quarkus.oidc.client-id} quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.value=${keycloak.credentials.secret} quarkus.oidc-client.oidc-client-for-graphql.credentials.client-secret.method=POST
如果您不指定 quarkus.oidc-client-graphql.client-name 属性,GraphQL 客户端将使用默认的 OIDC 客户端(无显式名称)。 |
特别对于类型安全的 GraphQL 客户端,您可以通过用 @io.quarkus.oidc.client.filter.OidcClientFilter
注解 GraphQLClientApi
接口来覆盖此设置,每个客户端一个。例如:
@GraphQLClientApi(configKey = "order-client")
@OidcClientFilter("oidc-client-for-graphql")
public interface OrdersGraphQLClient {
// Queries, mutations, and subscriptions go here.
}
要使用程序创建的 GraphQL 客户端来实现此目的,构建器(VertxDynamicGraphQLClientBuilder
和 VertxTypesafeGraphQLClientBuilder
)都包含一个 dynamicHeader(String, Uni<String>
) 方法,允许您插入一个可能在每次请求时都会更改的标头。要将其插入 OIDC 客户端,请使用:
@Inject
OidcClients oidcClients;
VertxTypesafeGraphQLClientBuilder builder = ....;
Uni<String> tokenUni = oidcClients.getClient("OIDC_CLIENT_NAME")
.getTokens().map(t -> "Bearer " + t.getAccessToken());
builder.dynamicHeader("Authorization", tokenUni);
VertxDynamicGraphQLClient client = builder.build();
配置参考
OIDC 客户端
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
布尔值 |
|
|
OpenID Connect (OIDC) 服务器的基础 URL,例如 环境变量: 显示更多 |
字符串 |
|
OIDC 端点的发现。如果未启用,您必须单独配置 OIDC 端点 URL。 环境变量: 显示更多 |
布尔值 |
|
OIDC 动态客户端注册端点的相对路径或绝对 URL。如果 环境变量: 显示更多 |
字符串 |
|
尝试初始连接到 OIDC 服务器的持续时间。例如,将持续时间设置为 环境变量: 显示更多 |
||
如果现有的 OIDC 连接暂时丢失,则重试重新建立连接的次数。与仅适用于初始连接尝试的 环境变量: 显示更多 |
整数 |
|
当前 OIDC 连接请求超时的秒数。 环境变量: 显示更多 |
|
|
是否应在工作线程上执行 DNS 查找。当您看到有关 HTTP 请求阻塞 Vert.x 事件循环的已记录警告时,请使用此选项。 环境变量: 显示更多 |
布尔值 |
|
WebClient 使用的连接池的最大大小。 环境变量: 显示更多 |
整数 |
|
当 WebClient 收到 HTTP 302 时自动遵循重定向。禁用此属性后,只允许单个重定向到完全相同的原始 URI,但前提是在重定向请求期间设置了一个或多个 cookie。 环境变量: 显示更多 |
布尔值 |
|
颁发访问令牌和刷新令牌的 OIDC 令牌端点;指定为相对路径或绝对 URL。如果 环境变量: 显示更多 |
字符串 |
|
OIDC 令牌吊销端点的相对路径或绝对 URL。 环境变量: 显示更多 |
字符串 |
|
应用程序的客户端 ID。每个应用程序都有一个客户端 ID,用于标识该应用程序。如果 环境变量: 显示更多 |
字符串 |
|
应用程序的客户端名称。它用于表示您在 OIDC 提供商的仪表板中注册应用程序(客户端)时可以提供的应用程序的可读描述。例如,您可以设置此属性以获得更具信息性的日志消息,这些消息记录给定客户端的活动。 环境变量: 显示更多 |
字符串 |
|
唯一的 OIDC 客户端标识符。创建动态 OIDC 客户端时必须设置此属性,在所有其他情况下它是可选的。 环境变量: 显示更多 |
字符串 |
|
如果此客户端配置已启用。 环境变量: 显示更多 |
布尔值 |
|
访问令牌范围列表 环境变量: 显示更多 |
字符串列表 |
|
刷新令牌时间偏移。如果启用了此属性,则配置的持续时间将转换为秒,并在检查访问令牌是否应刷新时加到当前时间。如果总和大于此访问令牌的过期时间,则将发生刷新。 环境变量: 显示更多 |
||
访问令牌过期时间,相对于当前时间。仅当访问令牌获取响应不包含访问令牌过期属性时,才会检查此属性。 环境变量: 显示更多 |
||
访问令牌过期时间偏移,可以添加到计算出的令牌过期时间。 环境变量: 显示更多 |
||
是否应将访问令牌 'expires_in' 属性检查为绝对时间值,而不是相对于当前时间的持续时间。 环境变量: 显示更多 |
布尔值 |
|
授予类型 环境变量: 显示更多 |
|
|
令牌授予响应中的访问令牌属性名 环境变量: 显示更多 |
字符串 |
|
令牌授予响应中的刷新令牌属性名 环境变量: 显示更多 |
字符串 |
|
令牌授予响应中的访问令牌过期时间属性名 环境变量: 显示更多 |
字符串 |
|
令牌授予响应中的刷新令牌过期时间属性名 环境变量: 显示更多 |
字符串 |
|
授权选项 环境变量: 显示更多 |
Map<String,Map<String,String>> |
|
要求所有使用 'OidcClient' 的过滤器在 post-construct 初始化时获取令牌,可能在这些令牌被使用之前很久。如果访问令牌可能在第一次使用之前过期且没有刷新令牌可用,则应禁用此属性。 环境变量: 显示更多 |
布尔值 |
|
必须发送到令牌端点的自定义 HTTP 标头 环境变量: 显示更多 |
Map<String,String> |
|
类型 |
默认 |
|
代理的主机名或 IP 地址。 环境变量: 显示更多 |
字符串 |
|
代理的端口号。默认值为 环境变量: 显示更多 |
整数 |
|
用户名(如果代理需要身份验证)。 环境变量: 显示更多 |
字符串 |
|
密码(如果代理需要身份验证)。 环境变量: 显示更多 |
字符串 |
|
类型 |
默认 |
|
要使用的 TLS 配置的名称。 如果配置了名称,它将使用来自 默认情况下,不使用默认 TLS 配置。 环境变量: 显示更多 |
字符串 |
|
类型 |
默认 |
|
环境变量: 显示更多 |
字符串 |
|
客户端密钥值。如果设置了 环境变量: 显示更多 |
字符串 |
|
CredentialsProvider bean 名称,只有在注册了多个 CredentialsProvider 时才应设置 环境变量: 显示更多 |
字符串 |
|
CredentialsProvider 的密钥库名称。当使用的 CredentialsProvider 需要密钥库名称来查找 secret 时,通常需要此密钥库名称,这在 CredentialsProvider 被多个扩展共享以从更动态的源(如 vault 实例或 secret manager)检索凭据时经常发生。 环境变量: 显示更多 |
字符串 |
|
CredentialsProvider 客户端密钥 环境变量: 显示更多 |
字符串 |
|
身份验证方法。如果设置了 环境变量: 显示更多 |
|
|
JWT 令牌源:OIDC 提供程序客户端或现有 JWT 持有者令牌。 环境变量: 显示更多 |
|
|
包含 JWT 持有者令牌的文件的路径,该令牌应用作客户端断言。仅当 JWT 源 ( 环境变量: 显示更多 |
path |
|
如果提供,则表示 JWT 使用密钥签名。它与 环境变量: 显示更多 |
字符串 |
|
CredentialsProvider bean 名称,只有在注册了多个 CredentialsProvider 时才应设置 环境变量: 显示更多 |
字符串 |
|
CredentialsProvider 的密钥库名称。当使用的 CredentialsProvider 需要密钥库名称来查找 secret 时,通常需要此密钥库名称,这在 CredentialsProvider 被多个扩展共享以从更动态的源(如 vault 实例或 secret manager)检索凭据时经常发生。 环境变量: 显示更多 |
字符串 |
|
CredentialsProvider 客户端密钥 环境变量: 显示更多 |
字符串 |
|
私钥的字符串表示。如果提供,表示 JWT 使用 PEM 或 JWK 格式的私钥进行签名。它与 环境变量: 显示更多 |
字符串 |
|
如果提供,表示 JWT 使用 PEM 或 JWK 格式的私钥进行签名。它与 环境变量: 显示更多 |
字符串 |
|
如果提供,则表示 JWT 使用密钥库中的私钥签名。它与 环境变量: 显示更多 |
字符串 |
|
用于指定密钥库文件密码的参数。 环境变量: 显示更多 |
字符串 |
|
私钥 ID 或别名。 环境变量: 显示更多 |
字符串 |
|
私钥密码。 环境变量: 显示更多 |
字符串 |
|
JWT 受众 ( 环境变量: 显示更多 |
字符串 |
|
作为 JWT 环境变量: 显示更多 |
字符串 |
|
作为 JWT 环境变量: 显示更多 |
字符串 |
|
作为 JWT 环境变量: 显示更多 |
字符串 |
|
其他声明。 环境变量: 显示更多 |
Map<String,String> |
|
用于 环境变量: 显示更多 |
字符串 |
|
JWT 生存期(以秒为单位)。此值添加到 JWT 颁发的时间,以计算到期时间。 环境变量: 显示更多 |
整数 |
|
如果为 true,则客户端身份验证令牌是 JWT 持有者授权断言。将仅生成 'assertion',而不是生成 'client_assertion' 和 'client_assertion_type' 表单属性。此选项仅受 OIDC 客户端扩展支持。 环境变量: 显示更多 |
布尔值 |
|
关于 Duration 格式
要编写持续时间值,请使用标准的 您还可以使用简化的格式,以数字开头
在其他情况下,简化格式将被转换为
|
OIDC 令牌传播
构建时固定的配置属性 - 所有其他配置属性都可以在运行时覆盖
配置属性 |
类型 |
默认 |
---|---|---|
如果 OIDC 令牌响应式传播已启用。 环境变量: 显示更多 |
布尔值 |
|
在 例如,您可能需要从 注意,此功能依赖于复制的上下文。有关 Vert.x 复制上下文的更多信息,请参阅 此指南。 环境变量: 显示更多 |
布尔值 |
|
使用 "urn:ietf:params:oauth:grant-type:token-exchange" 或 "urn:ietf:params:oauth:grant-type:jwt-bearer" 令牌授予,将当前令牌与 OpenId Connect 提供商交换以获取新令牌,然后再进行传播。 环境变量: 显示更多 |
布尔值 |
|
已配置的 OidcClient 的名称。注意,仅当启用了 环境变量: 显示更多 |
字符串 |