OpenID Connect (OIDC)客户端和令牌传播
摘要
向红帽构建的 Quarkus 文档提供反馈
要报告错误或改进文档,请登录您的红帽 JIRA 帐户并提交问题。如果您没有红帽 JIRA 帐户,系统会提示您创建一个帐户。
流程
- 单击以下链接 来创建 ticket。
- 在 Summary 中输入有关此问题的简单描述。
- 提供有关 描述 中问题或增强功能的详细描述。包括一个 URL,以在文档中发生问题。
- 点 Submit 创建问题并将其路由到适当的文档团队。
使开源包含更多
红帽致力于替换我们的代码、文档和 Web 属性中有问题的语言。我们从这四个术语开始:master、slave、黑名单和白名单。由于此项工作十分艰巨,这些更改将在即将推出的几个发行版本中逐步实施。详情请查看 CTO Chris Wright 的信息。
第 1 章 OpenID Connect (OIDC)和 OAuth2 客户端和过滤器
您可以使用 Quarkus 扩展进行 OpenID Connect 和 OAuth 2.0 访问令牌管理,专注于获取、刷新和传播令牌。
这包括以下内容:
-
使用
quarkus-oidc-client
,quarkus-oidc-client-reactive-filter
和quarkus-oidc-client-filter
扩展从 OpenID Connect 和 OAuth 2.0 兼容授权服务器(如 Keycloak )获取和刷新访问令牌。 -
使用
quarkus-oidc-token-propagation-reactive
和quarkus-oidc-token-propagation
扩展来传播当前的Bearer
或Authorization Code Flow
访问令牌。
由这些扩展管理的访问令牌可用作 HTTP 授权持有者令牌来访问远程服务。
另请参阅 OpenID Connect 客户端和令牌传播快速入门。
1.1. OidcClient
添加以下依赖项:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc-client</artifactId> </dependency>
quarkus-oidc-client
扩展提供 reactive io.quarkus.oidc.client.OidcClient
,它可用于使用 SmallRye Mutiny Uni
和 Vert.x WebClient
获取和刷新令牌。
OidcClient
在构建时初始化,使用 IDP 令牌端点 URL,该 URL 可以自动发现或手动配置。OidcClient
使用此端点通过利用令牌授权来获取访问令牌,如 client_credentials
或 password
,并使用 refresh_token
授权来刷新令牌。
1.1.1. 令牌端点配置
默认情况下,通过将 /.well-known/openid-configuration
路径添加到配置的 quarkus.oidc-client.auth-server-url
来发现令牌端点地址。
例如,给定这个 Keycloak URL:
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
OidcClient
将发现令牌端点 URL 为 http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens
。
或者,如果发现端点不可用,或者您希望在发现端点往返中保存,您可以禁用发现并配置令牌端点地址,并带有相对路径值。例如:
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus quarkus.oidc-client.discovery-enabled=false # Token endpoint: http://localhost: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=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens
在这种情况下,不需要设置 quarkus.oidc-client.auth-server-url
和 quarkus.oidc-client.discovery-enabled
。
1.1.2. 支持的令牌授予
主令牌授予 OidcClient
可用于获取令牌,是 client_credentials
(默认) 和密码
授权。
1.1.2.1. 客户端凭证授权
以下是如何将 OidcClient
配置为使用 client_credentials
授权:
quarkus.oidc-client.auth-server-url=http://localhost: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=http://localhost: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
1.1.2.2. 密码授权
以下是如何将 OidcClient
配置为使用 密码
授权:
quarkus.oidc-client.auth-server-url=http://localhost: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
配置前缀来进一步自定义,类似于如何自定义客户端凭证授权。
1.1.2.3. 其他授权
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=http://localhost: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
方法提供的刷新令牌来获取访问令牌。
使用 urn:ietf:params:oauth:grant-type:token-exchange
或 urn:ietf:params:oauth:grant-type:jwt-bearer
授权,如果您要构建复杂的微服务应用,并希望避免了多个服务被传播到并使用相同的 Bearer
令牌。如需了解更多详细信息,请参阅 MicroProfile RestClient 过滤器中的 MicroProfile RestClient Reactive 过滤器和 Token Propagation 中的 Token Propagation。
如果出于某种原因,可能需要使用 OidcClient
支持 授权代码授权
,因此您无法使用 Quarkus OIDC 扩展 来支持授权代码流。如果您实现授权代码流有很好的原因,您可以配置 OidcClient
,如下所示:
quarkus.oidc-client.auth-server-url=http://localhost: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
方法接受额外属性映射,并传递当前 代码和
redirect_uri
参数来交换令牌的授权代码。
OidcClient
还支持 urn:openid:params:grant-type:ciba
grant:
quarkus.oidc-client.auth-server-url=http://localhost: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
方法接受额外属性映射,并传递 auth_req_id
参数来交换令牌授权代码。
1.1.2.4. 授权范围
您可能需要请求特定的一组范围与发布的访问令牌关联。使用专用的 quarkus.oidc-client.scopes
list 属性,例如: quarkus.oidc-client.scopes=email,phone
1.1.3. 直接使用 OidcClient
一个可以直接使用 OidcClient
,如下所示:
import jakarta.inject.PostConstruct; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.Tokens; @Path("/service") public class OidcClientResource { @Inject OidcClient client; volatile Tokens currentTokens; @PostConstruct public void init() { currentTokens = client.getTokens().await().indefinitely(); } @GET public String getResponse() { Tokens tokens = currentTokens; if (tokens.isAccessTokenExpired()) { // Add @Blocking method annotation if this code is used with Reactive RestClient tokens = client.refreshTokens(tokens.getRefreshToken()).await().indefinitely(); currentTokens = tokens; } // Use tokens.getAccessToken() to configure MP RestClient Authorization header/etc } }
1.1.4. 注入令牌
您可以在内部 注入
使用 OidcClient
的令牌。令牌
可用于获取访问令牌并在需要时刷新它们:
import jakarta.inject.PostConstruct; import jakarta.inject.Inject; import jakarta.ws.rs.GET; 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 } }
1.1.5. use OidcClients
io.quarkus.oidc.client.OidcClients
是 OidcClient
s 的容器 - 它包括了一个默认的 OidcClient
和 named 客户端,可以配置如下:
quarkus.oidc-client.client-enabled=false quarkus.oidc-client.jwt-secret.auth-server-url=http://localhost: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 jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; @Path("/clients") public class OidcClientResource { @Inject OidcClients clients; @GET public String getResponse() { OidcClient client = clients.getClient("jwt-secret"); //Use this client to get the token } }
如果您也使用 OIDC 多租户,并且每个 OIDC 租户都有自己的关联的 OidcClient
,您可以使用 Vert.x RoutingContext
tenantId
属性。例如:
import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; import io.vertx.ext.web.RoutingContext; @Path("/clients") public class OidcClientResource { @Inject OidcClients clients; @Inject RoutingContext context; @GET public String getResponse() { String tenantId = context.get("tenantId"); // named OIDC tenant and client configurations use the same key: OidcClient client = clients.getClient(tenantId); //Use this client to get the token } }
如果需要,您也可以以编程方式创建新的 OidcClient
,如下所示:
import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import io.quarkus.oidc.client.OidcClient; import io.quarkus.oidc.client.OidcClients; import io.quarkus.oidc.client.OidcClientConfig; import io.smallrye.mutiny.Uni; @Path("/clients") public class OidcClientResource { @Inject OidcClients clients; @GET public String getResponse() { OidcClientConfig cfg = new OidcClientConfig(); cfg.setId("myclient"); cfg.setAuthServerUrl("http://localhost:8081/auth/realms/quarkus/"); cfg.setClientId("quarkus"); cfg.getCredentials().setSecret("secret"); Uni<OidcClient> client = clients.newClient(cfg); //Use this client to get the token } }
1.1.6. 注入名为 OidcClient 和 token
如果有多个配置的 OidcClient
对象,您可以通过额外的 qualifier @Named
指定 OidcClient 注入目标,而不是使用 OidcClient
OidcClients
:
package io.quarkus.oidc.client; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; @Path("/clients") public class OidcClientResource { @Inject @NamedOidcClient("jwt-secret") OidcClient client; @GET public String getResponse() { //Use the client to get the token } }
相同的限定符可以用来指定用于令牌注入的 Oidc
:
Client
@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()); } }
1.1.7. 在 RestClient Reactive ClientFilter 中使用 OidcClient
添加以下 Maven 依赖:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc-client-reactive-filter</artifactId> </dependency>
请注意,它还将出现 io.quarkus:quarkus-oidc-client
。
quarkus-oidc-client-reactive-filter
扩展提供 io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter
。
它的工作方式与 OidcClientRequestFilter
的效果类似(请参阅 MicroProfile RestClient 客户端过滤器 中的 OidcClient)- 它使用 OidcClient
获取访问令牌,根据需要刷新该访问令牌,并将它设置为 HTTP Authorization
Bearer
方案值。区别在于,它与 Reactive RestClient 配合使用,并实施非阻塞客户端过滤器,在获取或刷新令牌时不会阻断当前的 IO 线程。
OidcClientRequestReactiveFilter
延迟初始令牌获取,直到执行前,以避免阻塞 IO 线程。
您可以使用 io.quarkus.oidc.reactive.filter.OidcClientFilter 或
注解来选择性地注册 org.eclipse.microprofile.
rest.client.annotation.RegisterProviderOidcClientRequestReactiveFilter
:
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.smallrye.mutiny.Uni; @RegisterRestClient @OidcClientFilter @Path("/") public interface ProtectedResourceService { @GET Uni<String> getUserName(); }
or
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; @RegisterRestClient @RegisterProvider(OidcClientRequestReactiveFilter.class) @Path("/") public interface ProtectedResourceService { @GET Uni<String> getUserName(); }
OidcClientRequestReactiveFilter
默认使用默认的 OidcClient
。可以使用 quarkus.oidc-client-reactive-filter.client-name
配置属性来选择命名的 OidcClient
。您还可以通过设置 @
注释的 OidcClient
Filtervalue
属性来选择 OidcClient。通过注解设置的客户端名称的优先级高于 quarkus.oidc-client-reactive-filter.client-name
配置属性。例如,假设 这个 jwt-secret
名为 OIDC 客户端声明,您可以引用此客户端,如下所示:
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientFilter; import io.smallrye.mutiny.Uni; @RegisterRestClient @OidcClientFilter("jwt-secret") @Path("/") public interface ProtectedResourceService { @GET Uni<String> getUserName(); }
1.1.8. 在 RestClient ClientFilter 中使用 OidcClient
添加以下 Maven 依赖:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc-client-filter</artifactId> </dependency>
请注意,它还将出现 io.quarkus:quarkus-oidc-client
。
quarkus-oidc-client-filter
扩展提供 io.quarkus.oidc.client.filter.OidcClientRequestFilter
Jakarta REST ClientRequestFilter,它使用 OidcClient
获取访问令牌,根据需要刷新它,并将其设置为 HTTP Authorization
Bearer
方案值。
默认情况下,此过滤器将获取 OidcClient
,以便在初始化时获取第一对访问和刷新令牌。如果访问令牌短且刷新令牌不可用,则令牌获取应该会延迟为 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; @RegisterRestClient @OidcClientFilter @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientRequestFilter; @RegisterRestClient @RegisterProvider(OidcClientRequestFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
另外,如果设置了 quarkus.oidc-client-filter.register-filter=true
属性,OidcClientRequestFilter
可以自动注册到所有 MP Rest 或 Jakarta REST 客户端。
OidcClientRequestFilter
默认使用默认的 OidcClient
。可以使用 quarkus.oidc-client-filter.client-name
配置属性来选择命名的 OidcClient
。您还可以通过设置 @
注释的 OidcClient
Filtervalue
属性来选择 OidcClient。通过注解设置的客户端名称的优先级高于 quarkus.oidc-client-filter.client-name
配置属性。例如,假设 这个 jwt-secret
名为 OIDC 客户端声明,您可以引用此客户端,如下所示:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.client.filter.OidcClientFilter; @RegisterRestClient @OidcClientFilter("jwt-secret") @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
1.1.9. 使用自定义 RestClient ClientFilter
如果您愿意,您可以使用自己的自定义过滤器并注入 Tokens
:
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 和 Tokens 的 Inject
1.1.10. 刷新访问令牌
OidcClientRequestReactiveFilter
、OidcClientRequestFilter
和 Tokens
producers 将刷新当前的过期访问令牌(如果刷新令牌可用)。另外,quarkus.oidc-client.refresh-token-time-skew
属性可用于抢占访问令牌刷新,以避免发送可能导致 HTTP 401 错误的几乎过期的访问令牌。例如,如果此属性设置为 3S
,并且访问令牌将在 3 秒内过期,则此令牌将被自动刷新。
如果需要刷新访问令牌,但没有提供刷新令牌,则会尝试使用配置的授权(如 client_credentials
)获取新令牌。
有些 OpenID Connect 供应商不会在 client_credentials
授权响应中返回刷新令牌。例如,从 Keycloak 12 开始,client_credentials
默认不会返回刷新令牌。供应商也可以限制使用刷新令牌的次数。
1.1.11. 撤销访问令牌
如果您的 OpenId Connect 供应商(如 Keycloak)支持令牌撤销端点,则使用 OidcClient SerialrevokeAccessToken
来撤销当前的访问令牌。吊销端点 URL 与令牌请求 URI 一起发现,也可以使用 quarkus.oidc-client.revoke-path
配置。
如果将此令牌与 REST 客户端搭配使用,或者访问令牌已用于很长时间且您要刷新它,您可能希望撤销访问令牌。
这可以通过使用刷新令牌请求令牌刷新来实现。但是,如果刷新令牌不可用,您可以首先撤销它,然后请求新的访问令牌来刷新它。
1.1.12. OidcClient 身份验证
OidcClient
必须向 OpenID Connect Provider 进行身份验证,以便 client_credentials
和其他授权请求成功。所有 OIDC 客户端身份验证 选项都被支持,例如:
client_secret_basic
:
quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.secret=mysecret
or
quarkus.oidc-client.auth-server-url=http://localhost: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=http://localhost: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 # 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=http://localhost: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=http://localhost: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=http://localhost: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 # 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 密钥文件,签名算法为 RS256
:
quarkus.oidc-client.auth-server-url=http://localhost: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=http://localhost:8180/auth/realms/quarkus/ quarkus.oidc-client.client-id=quarkus-app quarkus.oidc-client.credentials.jwt.key-store-file=keystore.jks 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 overwire。
1.1.12.1. 其他 JWT 身份验证选项
如果使用 client_secret_jwt
或 private_key_jwt
身份验证方法,则可以自定义 JWT 签名算法、密钥标识符、audience、subject 和 issuer,例如:
# private_key_jwt client authentication quarkus.oidc-client.auth-server-url=http://localhost: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
1.1.12.2. Apple POST JWT
Apple OpenID Connect Provider 使用 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}
1.1.12.3. 双向 TLS
有些 OpenID Connect 提供者要求客户端作为 mutual TLS (mTLS
)身份验证过程的一部分进行身份验证。
quarkus-oidc-client
可以配置如下,以支持 mTLS
:
quarkus.oidc-client.tls.verification=certificate-validation # Keystore configuration quarkus.oidc-client.tls.key-store-file=client-keystore.jks quarkus.oidc-client.tls.key-store-password=${key-store-password} # Add more keystore properties if needed: #quarkus.oidc-client.tls.key-store-alias=keyAlias #quarkus.oidc-client.tls.key-store-alias-password=keyAliasPassword # Truststore configuration quarkus.oidc-client.tls.trust-store-file=client-truststore.jks quarkus.oidc-client.tls.trust-store-password=${trust-store-password} # Add more truststore properties if needed: #quarkus.oidc-client.tls.trust-store-alias=certAlias
1.1.13. 测试
首先,将以下依赖项添加到 test 项目中:
<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>
1.1.13.1. Wiremock
在您的测试项目中添加以下依赖项:
<dependency> <groupId>org.wiremock</groupId> <artifactId>wiremock</artifactId> <scope>test</scope> </dependency>
编写基于 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 测试端点。您可以使用注入的 MP REST 客户端和注册的 OidcClient 过滤器测试前端端点,调用下游端点。此端点将令牌回显。例如,请参阅 主
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} 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 秒后过期。使用 等待
静默等待大约 5 秒,现在下一个测试调用应返回 access_token_2
访问令牌,该令牌确认已过期的 access_token_1
访问令牌已被刷新。
1.1.13.2. Keycloak
如果使用 Keycloak,您可以使用 OpenID Connect Bearer Token Integration Keycloak 部分中描述的相同方法。
1.1.14. 如何检查日志中的错误
启用 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
1.2. OIDC 请求过滤器
您可以通过注册一个或多个 OidcRequestFilter
实现(可以更新或添加新的请求标头)来过滤 Quarkus 向 OIDC 供应商发出的 OIDC 请求。例如,过滤器可以分析请求正文,并将其摘要添加为新的标头值:
package io.quarkus.it.keycloak; import jakarta.enterprise.context.ApplicationScoped; import io.quarkus.arc.Unremovable; import io.quarkus.oidc.common.OidcRequestContextProperties; import io.quarkus.oidc.common.OidcRequestFilter; import io.vertx.core.http.HttpMethod; import io.vertx.mutiny.core.buffer.Buffer; import io.vertx.mutiny.ext.web.client.HttpRequest; @ApplicationScoped @Unremovable public class OidcRequestCustomizer implements OidcRequestFilter { @Override public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProperties) { HttpMethod method = request.method(); String uri = request.uri(); if (method == HttpMethod.POST && uri.endsWith("/service") && buffer != null) { request.putHeader("Digest", calculateDigest(buffer.toString())); } } private String calculateDigest(String bodyString) { // Apply the required digest algorithm to the body string } }
1.3. Token Propagation Reactive
quarkus-oidc-token-propagation-reactive
扩展提供了一个 RestEasy Reactive Client, io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter
,它简化了身份验证信息的传播。此客户端传播当前活动请求中存在的 bearer 令牌,或作为 HTTP Authorization
标头的 Bearer
方案值从 授权代码流机制 获取的令牌。
您可以使用 io.quarkus.oidc.token.propagation.AccessToken
或 org.eclipse.microprofile.rest.client.annotation.RegisterProvider
注解来选择性地注册 AccessTokenRequestReactiveFilter
,例如:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.AccessToken; @RegisterRestClient @AccessToken @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter; @RegisterRestClient @RegisterProvider(AccessTokenRequestReactiveFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
另外,AccessTokenRequestReactiveFilter
可以支持在传播令牌前交换令牌的复杂应用程序。
如果您使用 Keycloak 或其他支持 Token Exchange 令牌授权的 OIDC 供应商,您可以配置 AccessTokenRequestReactiveFilter
来交换令牌,如下所示:
quarkus.oidc-client.auth-server-url=http://localhost: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.oidc-token-propagation.exchange-token=true 1
- 1
- 请注意,当 OidcClient 名称使用
io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient
注解属性设置时,会忽略exchange-token
配置属性。
Note AccessTokenRequestReactiveFilter
将使用 OidcClient
来交换当前令牌,您可以使用 quarkus.oidc-client.grant-options.exchange
来设置 OpenID Connect Provider 所需的额外交换属性。
如果您使用需要使用 JWT bearer 令牌 授权的 Azure
等提供程序来交换当前令牌,您可以配置 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.oidc-token-propagation-reactive.exchange-token=true
AccessTokenRequestReactiveFilter
默认使用默认的 OidcClient
。可以使用 quarkus.oidc-token-propagation-reactive.client-name
配置属性或 io.quarkus.oidc.token.propagation.AccessToken failingTokenClient 注解属性来选择命名的
。
OidcClient
1.4. 令牌传播
quarkus-oidc-token-propagation
扩展提供了两个 Jakarta REST jakarta.ws.rs.client.ClientRequestFilter
类实现,以简化身份验证信息的传播。io.quarkus.oidc.token.propagation.AccessTokenRequestFilter
传播在当前活动请求中的 Bearer 令牌,或从 Authorization 代码流机制获取的令牌,作为 HTTP Authorization
标头的 Bearer
方案值。io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter
提供相同的功能,但也提供对 JWT 令牌的支持。
当您需要传播当前的授权代码流访问令牌时,直接令牌传播将正常工作,因为代码流访问令牌(而不是 ID 令牌)旨在为当前的 Quarkus 端点传播,以代表当前经过身份验证的用户访问远程服务。
但是,应避免直接端到端的 Bearer 令牌传播。例如,Client → Service A → Service B
,其中 Service B
接收由 Client
发送到 Service A
的令牌。在这种情况下,Service B
无法区分令牌是否来自 Service A
或直接与 客户端
。对于 Service B
验证令牌来自 Service A
,它应该能够断言新的签发者和受众声明。
此外,复杂的应用可能需要在传播令牌之前交换或更新令牌。例如,当 Service A
访问 Service B
时,访问上下文可能会有所不同。在这种情况下,服务 A
可能会被授予一个范围范围,或者完全不同的范围来访问服务 B
。
以下小节演示了 AccessTokenRequestFilter
和 JsonWebTokenRequestFilter
可以如何提供帮助。
1.4.1. RestClient AccessTokenRequestFilter
AccessTokenRequestFilter
将所有令牌视为 Strings,因此它可以使用 JWT 和 opaque 令牌。
您可以使用 io.quarkus.oidc.token.propagation.AccessToken
或 org.eclipse.microprofile.rest.client.annotation.RegisterProvider
来选择性地注册 AccessTokenRequestFilter
,例如:
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.AccessToken; @RegisterRestClient @AccessToken @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter; @RegisterRestClient @RegisterProvider(AccessTokenRequestFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
另外,如果 quarkus.oidc-token-propagation.register-filter
属性设置为 true
,并且 quarkus.oidc-token-propagation.json-web-token
属性设置为 false
,则 AccessTokenRequestFilter
会自动注册到所有 MP Rest 或 Jakarta REST 客户端。
1.4.1.1. 在传播前交换令牌
如果需要在传播前交换当前的访问令牌,并使用 Keycloak 或其他支持 Token Exchange 令牌授权的 OpenID Connect 供应商,您可以配置 AccessTokenRequestFilter
,如下所示:
quarkus.oidc-client.auth-server-url=http://localhost: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.oidc-token-propagation.exchange-token=true
如果您使用需要使用 JWT bearer 令牌 授权的 Azure
等提供程序来交换当前令牌,您可以配置 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.oidc-token-propagation.exchange-token=true
Note AccessTokenRequestFilter
将使用 OidcClient
来交换当前令牌,您可以使用 quarkus.oidc-client.grant-options.exchange
设置 OpenID Connect 提供程序期望的额外交换属性。
AccessTokenRequestFilter
默认使用 OidcClient
。可以使用 quarkus.oidc-token-propagation.client-name
配置属性来选择命名的 OidcClient
。
1.4.2. RestClient JsonWebTokenRequestFilter
如果使用 Bearer JWT 令牌,则建议使用 JsonWebTokenRequestFilter
,其中这些令牌可以具有其声明,如 签发者
和 使用者
修改,以及再次修改更新的令牌(如重新签名)。它需要一个注入的 org.eclipse.microprofile.jwt.JsonWebToken
,因此无法使用不透明令牌。另外,如果您的 OpenID Connect Provider 支持 Token Exchange 协议,则建议使用 AccessTokenRequestFilter
,因为 JWT 和不透明 bearer 令牌都可以安全地与 AccessTokenRequestFilter
交换。
JsonWebTokenRequestFilter
可使 Service A
实施轻松更新注入的 org.eclipse.microprofile.jwt.WebToken
及新的 签发者
和 audience
声明值,并使用新的签名再次保护更新的令牌。唯一的难度是确保服务 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; @RegisterRestClient @JsonWebToken @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
or
import org.eclipse.microprofile.rest.client.annotation.RegisterProvider; import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter; @RegisterRestClient @RegisterProvider(JsonWebTokenRequestFilter.class) @Path("/") public interface ProtectedResourceService { @GET String getUserName(); }
另外,如果 quarkus.oidc-token-propagation.register-filter
和 quarkus.oidc-token-propagation.oidc-token-propagation.json-web-token
属性都会自动注册 JsonWebTokenRequestFilter
。
1.4.2.1. 在传播前更新令牌
如果注入的令牌需要有其 iss
(issuer)或 aud
(audience)声明使用新签名再次更新和保护,则可以配置 JsonWebTokenRequestFilter
,如下所示:
quarkus.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 或支持 Token Exchange 协议的 OpenID Connect Provider,请使用 AccessTokenRequestFilter
。
1.4.3. 测试
您可以生成令牌,如 OpenID Connect Bearer Token Integration 测试 部分中所述。准备 REST 测试端点。您可以使用测试前端端点,它使用注入的 MP REST 客户端和注册的令牌传播过滤器,调用下游端点。例如,请参阅 主
Quarkus 存储库中的 integration-tests/oidc-token-propagation
。
1.5. Token Propagation Reactive
添加以下 Maven 依赖:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc-token-propagation-reactive</artifactId> </dependency>
quarkus-oidc-token-propagation-reactive
扩展提供 io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter
,可用于传播当前的 Bearer
或 Authorization Code Flow
访问令牌。
quarkus-oidc-token-propagation-reactive
扩展(与非reactive quarkus-oidc-token-propagation
扩展相反)目前不支持在传播前对令牌进行交换或重新签名。但是,这些功能可能会在以后添加。
1.6. 参考
第 2 章 OpenID Connect 客户端和令牌传播快速入门
了解如何使用 OpenID Connect (OIDC)和带有过滤器的 OAuth2 客户端在应用程序中获取、刷新和传播访问令牌。
有关 Quarkus 中 OIDC 客户端和
令牌传播支持的更多信息
,请参阅 OpenID Connect (OIDC)和 OAuth2 客户端和过滤器参考指南。
要使用 Bearer Token Authorization 保护应用程序,请参阅 OpenID Connect (OIDC) Bearer 令牌身份验证 指南。
2.1. 先决条件
要完成本指南,您需要:
- 大约 15 分钟
- IDE
-
正确配置了
JAVA_HOME
的 JDK 17+ - Apache Maven 3.9.6
- 正常工作的容器运行时(Docker 或 Podman)
- 如果要使用 Quarkus CLI,可选
- 如果要构建原生可执行文件(或者使用原生容器构建,则可选的 Mandrel 或 GraalVM) https://quarkus.io/version/3.8/guides/building-native-image#configuring-graalvm
- jq 工具
2.2. 架构
在本例中,应用程序使用两个 Jakarta REST 资源构建,即 FrontendResource
和 ProtectedResource
。在这里,Frontend Resource
使用两种方法之一将访问令牌传播到 ProtectedResource
:
- 它可以通过在传播令牌前使用 OIDC 令牌传播重新主动过滤器来获取令牌。
- 它可以使用 OIDC 令牌传播 Reactive 过滤器传播传入的访问令牌。
FrontendResource
有四个端点:
-
/frontend/user-name-with-oidc-client-token
-
/frontend/admin-name-with-oidc-client-token
-
/frontend/user-name-with-propagated-token
-
/frontend/admin-name-with-propagated-token
FrontendResource
使用带有 OIDC 令牌传播 Reactive 过滤器的 REST 客户端来获取,并在 /frontend/user-name-with-oidc-client-token
或 /frontend/admin-name-with-oidc-client-token
被调用时将访问令牌传播到 ProtectedResource
。另外,FrontendResource
使用带有 OpenID Connect Token Propagation Reactive Filter
的 REST 客户端,在 /frontend/user-name-with-propagated-token
或 /frontend/admin-name-with-propagated-token
被调用时,将当前的传入访问令牌传播到 ProtectedResource
。
ProtectedResource
有两个端点:
-
/protected/user-name
-
/protected/admin-name
两个端点返回从传入访问令牌中提取的用户名,该令牌从 FrontendResource
传播到 ProtectedResource
。这些端点之间的唯一区别在于,只有在当前访问令牌具有用户角色时才允许调用 /protected/
,只有在当前访问令牌具有 user
-nameadmin
角色时才允许调用 /protected/admin-name
。
2.3. 解决方案
我们建议您按照下一小节中的说明进行操作,并逐步创建应用程序步骤。但是,您可以进入已完成的示例。
克隆 Git 存储库: git clone https://github.com/quarkusio/quarkus-quickstarts.git -b 3.8
,或下载 存档。
解决方案位于 security-openid-connect-client-quickstart
目录中。
2.4. 创建 Maven 项目
首先,您需要一个新项目。使用以下命令创建新项目:
使用 Quarkus CLI:
quarkus create app org.acme:security-openid-connect-client-quickstart \ --extension='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \ --no-code cd security-openid-connect-client-quickstart
要创建 Gradle 项目,请添加--
gradle or
--gradle-kotlin-dsl
选项。有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。
使用 Maven:
mvn io.quarkus.platform:quarkus-maven-plugin:3.8.5:create \ -DprojectGroupId=org.acme \ -DprojectArtifactId=security-openid-connect-client-quickstart \ -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \ -DnoCode cd security-openid-connect-client-quickstart
要创建 Gradle 项目,请添加
-DbuildTool=gradle
or-DbuildTool=gradle-kotlin-dsl
选项。
对于 Windows 用户:
-
如果使用 cmd,(不要使用反向斜杠
\
并将所有内容放在同一行中) -
如果使用 Powershell,则双引号中的 wrap
-D
参数,如"-DprojectArtifactId=security-openid-connect-client-quickstart"
此命令生成 Maven 项目,导入
、oidc
-client-reactive-filteroidc-token-propagation-reactive-filter
、resteasy-reactive
扩展。
如果您已经配置了 Quarkus 项目,您可以在项目基本目录中运行以下命令来将这些扩展添加到项目中:
使用 Quarkus CLI:
quarkus extension add oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive
使用 Maven:
./mvnw quarkus:add-extension -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
使用 Gradle:
./gradlew addExtension --extensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
这个命令在构建文件中添加以下扩展:
使用 Maven:
<dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc-client-reactive-filter</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-oidc-token-propagation-reactive</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-resteasy-reactive</artifactId> </dependency>
使用 Gradle:
implementation("io.quarkus:quarkus-oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive")
2.5. 编写应用程序
首先实施 ProtectedResource
:
package org.acme.security.openid.connect.client; import jakarta.annotation.security.RolesAllowed; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import io.quarkus.security.Authenticated; import io.smallrye.mutiny.Uni; import org.eclipse.microprofile.jwt.JsonWebToken; @Path("/protected") @Authenticated public class ProtectedResource { @Inject JsonWebToken principal; @GET @RolesAllowed("user") @Produces("text/plain") @Path("userName") public Uni<String> userName() { return Uni.createFrom().item(principal.getName()); } @GET @RolesAllowed("admin") @Produces("text/plain") @Path("adminName") public Uni<String> adminName() { return Uni.createFrom().item(principal.getName()); } }
ProtectedResource
返回一个来自 userName ()
和 adminName ()
方法的名称。名称从当前的 JsonWebToken
中提取。
接下来,添加两个 REST 客户端: OidcClientRequestReactiveFilter
和 AccessTokenRequestReactiveFilter
,其 FrontendResource
用来调用 ProtectedResource
。
添加 OidcClientRequestReactiveFilter
REST 客户端:
package org.acme.security.openid.connect.client; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; 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; @RegisterRestClient @RegisterProvider(OidcClientRequestReactiveFilter.class) @Path("/") public interface RestClientWithOidcClientFilter { @GET @Produces("text/plain") @Path("userName") Uni<String> getUserName(); @GET @Produces("text/plain") @Path("adminName") Uni<String> getAdminName(); }
RestClientWithOidcClientFilter
接口依赖于 OidcClientRequestReactiveFilter
来获取和传播令牌。
添加 AccessTokenRequestReactiveFilter
REST 客户端:
package org.acme.security.openid.connect.client; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; 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 io.smallrye.mutiny.Uni; @RegisterRestClient @RegisterProvider(AccessTokenRequestReactiveFilter.class) @Path("/") public interface RestClientWithTokenPropagationFilter { @GET @Produces("text/plain") @Path("userName") Uni<String> getUserName(); @GET @Produces("text/plain") @Path("adminName") Uni<String> getAdminName(); }
RestClientWithTokenPropagationFilter
接口依赖于 AccessTokenRequestReactiveFilter
来传播传入的已存在的令牌。
请注意,RestClientWithOidcClientFilter
和 RestClientWithTokenPropagationFilter
接口都是相同的。这是因为在同一 REST 客户端上组合 OidcClientRequestReactiveFilter
和 AccessTokenRequestReactiveFilter
会导致副作用,因为两个过滤器可能会相互干扰。例如,OidcClientRequestReactiveFilter
可以覆盖 AccessTokenRequestReactiveFilter
传播的令牌,或者 AccessTokenRequestReactiveFilter
可能会在没有令牌可用时被调用,并且 OidcClientRequestReactiveFilter
会改为获取新令牌。
现在,通过添加 FrontendResource
来完成创建应用程序:
package org.acme.security.openid.connect.client; import jakarta.inject.Inject; import jakarta.ws.rs.GET; import jakarta.ws.rs.Path; import jakarta.ws.rs.Produces; import org.eclipse.microprofile.rest.client.inject.RestClient; import io.smallrye.mutiny.Uni; @Path("/frontend") public class FrontendResource { @Inject @RestClient RestClientWithOidcClientFilter restClientWithOidcClientFilter; @Inject @RestClient RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter; @GET @Path("user-name-with-oidc-client-token") @Produces("text/plain") public Uni<String> getUserNameWithOidcClientToken() { return restClientWithOidcClientFilter.getUserName(); } @GET @Path("admin-name-with-oidc-client-token") @Produces("text/plain") public Uni<String> getAdminNameWithOidcClientToken() { return restClientWithOidcClientFilter.getAdminName(); } @GET @Path("user-name-with-propagated-token") @Produces("text/plain") public Uni<String> getUserNameWithPropagatedToken() { return restClientWithTokenPropagationFilter.getUserName(); } @GET @Path("admin-name-with-propagated-token") @Produces("text/plain") public Uni<String> getAdminNameWithPropagatedToken() { return restClientWithTokenPropagationFilter.getAdminName(); } }
FrontendResource
使用带有 OIDC 令牌传播 Reactive 过滤器的 REST 客户端,在 /frontend/user-name-with-oidc-client-token
或 /frontend/admin-name-with-oidc-client-token
被调用时,获取并传播访问令牌到 ProtectedResource
。另外,FrontendResource
使用带有 OpenID Connect Token Propagation Reactive Filter
的 REST 客户端在 /frontend/user-name-with-propagated-token
或 /frontend/admin-name-with-propagated-token
被调用时将当前的传入访问令牌传播到 ProtectedResource
。
最后,添加 Jakarta REST ExceptionMapper
:
package org.acme.security.openid.connect.client; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.ext.ExceptionMapper; import jakarta.ws.rs.ext.Provider; import org.jboss.resteasy.reactive.ClientWebApplicationException; @Provider public class FrontendExceptionMapper implements ExceptionMapper<ClientWebApplicationException> { @Override public Response toResponse(ClientWebApplicationException t) { return Response.status(t.getResponse().getStatus()).build(); } }
此例外映射程序仅用于在测试期间验证 ProtectedResource
是否在令牌没有预期的角色时返回 403
。如果没有此映射程序,Easy Reactive
可以正确地将来自 REST 客户端调用的转义转换为 500
,以避免泄漏下游资源(如 ProtectedResource
)的信息。但是,在测试中,无法断言 500
是由授权异常导致的,而不是一些内部错误。
2.6. 配置应用程序
准备代码,您可以配置应用程序:
# Configure OIDC %prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus quarkus.oidc.client-id=backend-service quarkus.oidc.credentials.secret=secret # Tell Dev Services for Keycloak to import the realm file # This property is ineffective when running the application in JVM or Native modes but only in dev and test modes. quarkus.keycloak.devservices.realm-path=quarkus-realm.json # Configure OIDC Client quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url} quarkus.oidc-client.client-id=${quarkus.oidc.client-id} quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret} quarkus.oidc-client.grant.type=password quarkus.oidc-client.grant-options.password.username=alice quarkus.oidc-client.grant-options.password.password=alice # Configure REST clients %prod.port=8080 %dev.port=8080 %test.port=8081 org.acme.security.openid.connect.client.RestClientWithOidcClientFilter/mp-rest/url=http://localhost:${port}/protected org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected
此配置引用 Keycloak,由 ProtectedResource
用于验证传入的访问令牌,以及 OidcClient
使用密码
授权来获取用户 alice
的令牌。两个 REST 客户端都指向"ProtectedResource"的 HTTP 地址。
将 %prod.
配置集前缀添加到 quarkus.oidc.auth-server-url
可确保当应用程序以 dev 或 test 模式运行时,为您启动 Dev Services
。有关更多信息,请参阅 以 dev 模式运行应用。
2.7. 启动并配置 Keycloak 服务器
当您以 dev 或 test 模式运行应用程序时,不要启动 Keycloak 服务器 ; Keycloak 的 Dev Services
启动容器。有关更多信息,请参阅 以 dev 模式运行应用。确保将 realm 配置文件放在 classpath 上,它位于 target/classes
目录中。此放置可确保文件在 dev 模式中自动导入。但是,如果您已构建了一个 完整的解决方案,则不需要将 realm 文件添加到类路径,因为构建过程已完成。
要启动 Keycloak 服务器,您可以使用 Docker 并运行以下命令:
docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
将 {keycloak.version}
设置为 24.0.0
或更高版本。
您可以在 localhost:8180 访问您的 Keycloak 服务器。
以 admin
用户身份登录,以访问 Keycloak 管理控制台。密码是 admin
。
导入 realm 配置文件以创建 新域。如需了解更多详细信息,请参阅关于如何创建新域的 Keycloak 文档。https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm
此 quarkus
域文件添加 frontend
客户端,以及 alice
和 admin
用户。alice
具有 用户角色
。admin
同时具有 user
和 admin
角色。
2.8. 以 dev 模式运行应用程序
要在 dev 模式下运行应用程序,请使用:
使用 Quarkus CLI:
quarkus dev
使用 Maven:
./mvnw quarkus:dev
使用 Gradle:
./gradlew --console=plain quarkusDev
Keycloak 的 dev Services 启动 Keycloak 容器,并导入 quarkus-realm.json
。
打开 /q/dev-ui 中的 Dev UI,再单击 OpenID Connect Dev UI 卡中的 Provider: Keycloak
链接。
提示时,登录到 OpenID Connect Dev UI 提供的 单个页面应用程序
:
以
alice
身份登录,密码为alice
。此用户具有用户角色
。-
访问
/frontend/user-name-with-propagated-token
,它返回200
。 -
访问
/frontend/admin-name-with-propagated-token
,它返回403
。
-
访问
注销,然后以
admin
用户身份使用密码admin
重新登录。此用户同时具有admin
和 用户角色。-
访问
/frontend/user-name-with-propagated-token
,它返回200
。 -
访问
/frontend/admin-name-with-propagated-token
,它返回200
。
-
访问
在这种情况下,您要测试 FrontendResource
可以从 OpenID Connect Dev UI 传播访问令牌。
2.9. 在 JVM 模式下运行应用程序
在以 dev 模式探索应用后,您可以将其作为标准 Java 应用运行。
首先,编译它:
使用 Quarkus CLI:
quarkus build
使用 Maven:
./mvnw install
使用 Gradle:
./gradlew build
然后运行它:
java -jar target/quarkus-app/quarkus-run.jar
2.10. 以原生模式运行应用程序
您可以将此演示编译为原生代码,不需要修改。
这意味着,您不再需要在生产环境中安装 JVM,因为运行时技术包含在生成的二进制中,并优化以最小资源运行。
编译需要更长的时间,因此默认关闭此步骤。要再次构建,请启用 原生
配置集:
使用 Quarkus CLI:
quarkus build --native
使用 Maven:
./mvnw install -Dnative
使用 Gradle:
./gradlew build -Dquarkus.package.type=native
在稍等片刻后,构建完成后,您可以直接运行原生二进制文件:
./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
2.11. 测试应用程序
有关以 dev 模式测试应用程序的更多信息,请参阅前面 在 dev mode 中运行该应用。
您可以使用 curl
来测试以 JVM 或原生模式启动的应用程序。
获取 alice
的访问令牌:
export access_token=$(\ curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \ --user backend-service:secret \ -H 'content-type: application/x-www-form-urlencoded' \ -d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \ )
现在,使用此令牌调用 /frontend/user-name-with-propagated-token
和 /frontend/admin-name-with-propagated-token
:
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
此命令返回 200
状态代码和名称 alice
。
curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
相反,此命令会返回 403
。请记住,alice
仅具有 用户角色
。
接下来,获取 admin
的访问令牌:
export access_token=$(\ curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \ --user backend-service:secret \ -H 'content-type: application/x-www-form-urlencoded' \ -d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \ )
使用此令牌调用 /frontend/user-name-with-propagated-token
:
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
此命令返回 200
状态代码和名称 admin
。
现在,使用此令牌调用 /frontend/admin-name-with-propagated-token
:
curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-propagated-token \ -H "Authorization: Bearer "$access_token
此命令还会返回 200
状态代码和名称 admin,因为
具有 admin
user
和 admin
角色。
现在,检查 FrontendResource
方法,它不会传播现有的令牌,而是使用 OidcClient
获取和传播令牌。如前文所示,OidcClient
配置为获取 alice
用户的令牌,因此:
curl -i -X GET \ http://localhost:8080/frontend/user-name-with-oidc-client-token
此命令返回 200
状态代码和名称 alice
。
curl -i -X GET \ http://localhost:8080/frontend/admin-name-with-oidc-client-token
与前面的命令不同,此命令会返回 403
状态代码。