OpenID Connect (OIDC)客户端和令牌传播


Red Hat build of Quarkus 3.27

Red Hat Customer Content Services

摘要

本指南详细介绍了如何配置和使用 OpenID Connect (OIDC)客户端和令牌传播,包括令牌管理、请求过滤和应用程序设置。

您可以使用 Quarkus 扩展进行 OpenID Connect 和 OAuth 2.0 访问令牌管理,专注于获取、刷新和传播令牌。

这包括以下内容:

  • 使用 quarkus-oidc-client,quarkus-rest-client-oidc-filterquarkus-resteasy-client-oidc-filter 扩展从 OpenID Connect 和 OAuth 2.0 兼容授权服务器(如 Keycloak )获取和刷新访问令牌。
  • 使用 quarkus-rest-client-oidc-token-propagationquarkus-resteasy-client-oidc-token-propagation 扩展来传播当前的 BearerAuthorization Code Flow 访问令牌。

由这些扩展管理的访问令牌可用作 HTTP 授权持有者令牌来访问远程服务。

另请参阅 OpenID Connect 客户端和令牌传播快速入门

1.1. OidcClient

添加以下依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap

quarkus-oidc-client 扩展提供 reactive io.quarkus.oidc.client.OidcClient,它可用于使用 SmallRye Mutiny UniVert.x WebClient 获取和刷新令牌。

OidcClient 在构建时初始化,使用 IDP 令牌端点 URL,该 URL 可以自动发现或手动配置。OidcClient 使用此端点通过利用令牌授权来获取访问令牌,如 client_credentialspassword,并使用 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

在没有发现的情况下配置令牌端点 URL 的更紧凑方法是将 quarkus.oidc-client.token-path 设置为一个绝对 URL:

quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens
Copy to Clipboard Toggle word wrap

在这种情况下,不需要设置 quarkus.oidc-client.auth-server-urlquarkus.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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap
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
Copy to Clipboard Toggle word wrap

它可以通过使用 quarkus.oidc-client.grant-options.password 配置前缀来进一步自定义,类似于如何自定义客户端凭证授权。

1.1.2.3. 其他授权

OidcClient 还可以使用授权来帮助获取令牌,该令牌需要一些无法在配置中捕获的额外输入参数。这些授权是 refresh_token (使用外部刷新令牌)、authorization_code 和两个授权来交换当前访问令牌,即 urn:ietf:params:oauth:grant-type:token-exchangeurn: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
Copy to Clipboard Toggle word wrap

然后,您可以使用 OidcClient.refreshTokens 方法提供的刷新令牌来获取访问令牌。

使用 urn:ietf:params:oauth:grant-type:token-exchangeurn:ietf:params:oauth:grant-type:jwt-bearer 授权,如果您要构建复杂的微服务应用,并希望避免了多个服务被传播到并使用相同的 Bearer 令牌。如需了解更多详细信息,请参阅 Quarkus RESTToken Propagation for RESTEasy Classic 的令牌传播。

如果出于某种原因,可能需要使用 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
Copy to Clipboard Toggle word wrap

然后,您可以使用 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
Copy to Clipboard Toggle word wrap

然后,您可以使用 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 获取访问令牌,并将它们设置为 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);
}
Copy to Clipboard Toggle word wrap

现在,使用 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()));
    }
}
Copy to Clipboard Toggle word wrap
1
io.quarkus.oidc.client.runtime.TokensHelper 管理访问令牌获取和刷新。

1.1.4. 注入令牌

您可以在内部 注入 使用 OidcClient 的令牌。令牌 可用于获取访问令牌并在需要时刷新它们:

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
    }
}
Copy to Clipboard Toggle word wrap

1.1.5. use OidcClients

io.quarkus.oidc.client.OidcClientsOidcClients 的容器 - 它包括了一个默认的 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
Copy to Clipboard Toggle word wrap

在这种情况下,默认客户端使用 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()));
    }
}
Copy to Clipboard Toggle word wrap
1
请参阅 Use OidcClient directly 部分中的 RestClientWithTokenHeaderParam 声明。
注意

如果您也使用 OIDC 多租户,并且每个 OIDC 租户都有自己的关联的 OidcClient,您可以使用 Vert.x RoutingContext tenant-id 属性。例如:

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("tenant-id");
        // named OIDC tenant and client configurations use the same key:
        OidcClient client = clients.getClient(tenantId);
        //Use this client to get the token
    }
}
Copy to Clipboard Toggle word wrap

您还可以以编程方式创建新的 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);
    }
}
Copy to Clipboard Toggle word wrap

现在,您可以使用此客户端,如下所示:

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()));
    }
}
Copy to Clipboard Toggle word wrap
1
请参阅 Use OidcClient directly 部分中的 RestClientWithTokenHeaderParam 声明。

1.1.6. 注入名为 OidcClient 和 token

如果有多个配置的 OidcClient 对象,您可以通过额外的 qualifier @Named OidcClient 指定 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()));
    }
}
Copy to Clipboard Toggle word wrap
1
请参阅 Use OidcClient directly 部分中的 RestClientWithTokenHeaderParam 声明。

相同的限定符可以用来指定用于令牌注入的 Oidc Client

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());
    }
}
Copy to Clipboard Toggle word wrap

添加以下 Maven 依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-rest-client-oidc-filter</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap
注意

它还会使 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.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();
}
Copy to Clipboard Toggle word wrap

或者

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();
}
Copy to Clipboard Toggle word wrap

OidcClientRequestReactiveFilter 默认使用默认的 OidcClient。可以使用 quarkus.rest-client-oidc-filter.client-name 配置属性来选择命名的 OidcClient。您还可以通过设置 @ OidcClient Filter 注释的 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();
}
Copy to Clipboard Toggle word wrap

如果您还想在每次 ProtectedResourceService#getUserName 调用时刷新令牌,则会导致 401 Unauthorized 错误,请使用 quarkus.rest-client-oidc-filter.refresh-on-unauthorized 配置属性,如下例所示:

quarkus.rest-client-oidc-filter.refresh-on-unauthorized=true
Copy to Clipboard Toggle word wrap

另外,如果您只需要为单个端点启用此功能,请创建一个类似以下示例的自定义过滤器:

package io.quarkus.it.keycloak;

import io.quarkus.oidc.client.reactive.filter.runtime.AbstractOidcClientRequestReactiveFilter;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;

@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestReactiveFilter {

    @Override
    protected boolean refreshOnUnauthorized() {
        return true;
    }
}
Copy to Clipboard Toggle word wrap

1.1.8. 在 RestClient ClientFilter 中使用 OidcClient

添加以下 Maven 依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-resteasy-client-oidc-filter</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap
注意

它还会使 io.quarkus:quarkus-oidc-client

quarkus-resteasy-client-oidc-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.OidcClientFilterorg.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();
}
Copy to Clipboard Toggle word wrap

或者

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();
}
Copy to Clipboard Toggle word wrap

另外,如果设置了 quarkus.resteasy-client-oidc-filter.register-filter.register-filter=true 属性,OidcClientRequestFilter 可以自动注册到所有 MP Rest 或 Jakarta REST 客户端。

OidcClientRequestFilter 默认使用默认的 OidcClient。可以使用 quarkus.resteasy-client-oidc-filter.client-name 配置属性来选择命名的 OidcClient。您还可以通过设置 @ OidcClient Filter 注释的 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();
}
Copy to Clipboard Toggle word wrap

如果您还想在每次 ProtectedResourceService#getUserName 调用时刷新令牌,则会导致 401 Unauthorized 错误,请使用 quarkus.resteasy-client-oidc-filter.refresh-on-unauthorized 配置属性,如下例所示:

quarkus.resteasy-client-oidc-filter.refresh-on-unauthorized=true
Copy to Clipboard Toggle word wrap

另外,如果您只需要为单个端点启用此功能,请创建一个类似以下示例的自定义过滤器:

package io.quarkus.it.keycloak;

import io.quarkus.oidc.client.filter.runtime.AbstractOidcClientRequestFilter;
import jakarta.annotation.Priority;
import jakarta.ws.rs.Priorities;

@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter extends AbstractOidcClientRequestFilter {

    @Override
    protected boolean refreshOnUnauthorized() {
        return true;
    }
}
Copy to Clipboard Toggle word wrap

1.1.9. 使用自定义 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());
    }
}
Copy to Clipboard Toggle word wrap

Tokens 生成者将获取并刷新令牌,自定义过滤器将决定如何使用令牌。

您还可以注入命名的 Tokens,请参阅名为 OidcClient 和 Tokens 的 Inject

1.1.10. 刷新访问令牌

OidcClientRequestReactiveFilterOidcClientRequestFilterTokens producers 将刷新当前的过期访问令牌(如果刷新令牌可用)。另外,quarkus.oidc-client.refresh-token-time-skew 属性可用于抢占访问令牌刷新,以避免发送可能导致 HTTP 401 错误的几乎过期的访问令牌。例如,如果此属性设置为 3S,并且访问令牌将在 3 秒内过期,则此令牌将被自动刷新。

默认情况下,OIDC 客户端会在当前请求期间刷新令牌,当它检测到令牌已过期时,如果配置了 刷新令牌时间偏移,则几乎过期。性能关键应用程序可能需要避免在传入请求期间等待可能的令牌刷新,并配置异步令牌刷新,例如:

quarkus.oidc-client.refresh-interval=1m 
1
Copy to Clipboard Toggle word wrap
1
如果当前访问令牌已过期并且必须刷新,请每分钟检查。

如果需要刷新访问令牌,但没有提供刷新令牌,则会尝试使用配置的授权(如 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
Copy to Clipboard Toggle word wrap

或者

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
Copy to Clipboard Toggle word wrap

或者,使用从 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
# 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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

或者,使用从 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
# 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
Copy to Clipboard Toggle word wrap

private_key_jwt,带有内嵌在 application.properties 中的 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=Base64-encoded private key representation
Copy to Clipboard Toggle word wrap

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
Copy to Clipboard Toggle word wrap

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.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
Copy to Clipboard Toggle word wrap

使用 client_secret_jwtprivate_key_jwt 身份验证方法可确保没有客户端 secret overwire。

1.1.12.1. 其他 JWT 身份验证选项

如果使用 client_secret_jwtprivate_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
Copy to Clipboard Toggle word wrap
1.1.12.2. 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
Copy to Clipboard Toggle word wrap

接下来,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
Copy to Clipboard Toggle word wrap
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...");
    }
}
Copy to Clipboard Toggle word wrap

以下是 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...");
    }
}
Copy to Clipboard Toggle word wrap
1.1.12.3. 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}
Copy to Clipboard Toggle word wrap
1.1.12.4. 双向 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
Copy to Clipboard Toggle word wrap

1.1.13. OIDC 客户端 SPI

当您的自定义扩展必须使用 OIDC 令牌授予的 OIDC 令牌获取 OIDC 令牌时,此扩展只能依赖于 OIDC 客户端 SPI,并根据需要让 OIDC 客户端本身获取和刷新访问令牌。

添加以下依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client-spi</artifactId>
</dependency>
Copy to Clipboard Toggle word wrap

下一次更新您的扩展以使用 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();
   }
}
Copy to Clipboard Toggle word wrap

目前,io.quarkus.oidc.client.spi.TokenProvider 仅适用于默认的 OIDC 客户端,因为自定义扩展不太可能了解多个命名 OIDC 客户端。

1.1.14. 测试

首先,将以下依赖项添加到 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>
Copy to Clipboard Toggle word wrap
1.1.14.1. Wiremock

在您的测试项目中添加以下依赖项:

<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
    <version>${wiremock.version}</version> 
1

</dependency>
Copy to Clipboard Toggle word wrap
1
使用正确的 Wiremock 版本。可在此找到所有可用版本。https://search.maven.org/artifact/org.wiremock/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;
        }
    }
}
Copy to Clipboard Toggle word wrap

准备 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: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
Copy to Clipboard Toggle word wrap

最后,编写测试代码。根据上面的基于 Wiremock 的资源,第一次测试调用应返回 access_token_1 访问令牌,该令牌将在 4 秒后过期。使用 等待 静默等待大约 5 秒,现在下一个测试调用应返回 access_token_2 访问令牌,该令牌确认已过期的 access_token_1 访问令牌已被刷新。

1.1.14.2. Keycloak

如果使用 Keycloak,您可以使用 OpenID Connect Bearer Token Integration Keycloak 部分中描述的相同方法。

1.1.15. 如何检查日志中的错误

启用 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
Copy to Clipboard Toggle word wrap

启用 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
Copy to Clipboard Toggle word wrap

1.2. 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
    }
}
Copy to Clipboard Toggle word wrap

OidcRequestContextProperties 可用于访问请求属性。目前,您可以使用 client_id 键访问客户端租户 id,以及一个 grant_type 键来访问 OIDC 客户端用来获取令牌的授权类型。

OidcRequestFilter 可以通过准备 io.vertx.mutiny.core.buffer.Buffer 实例并在请求上下文上设置请求正文,例如:

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.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.mutiny.core.buffer.Buffer;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN)
public class TokenRequestFilter implements OidcRequestFilter {

    @Override
    public void filter(OidcRequestContext rc) {
        // Add more required properties to the token request
        rc.requestBody(rc.requestBody().toString() + "&opaque_token_param=opaque_token_value"));
    }
}
Copy to Clipboard Toggle word wrap

1.3. 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");
        }
    }

}
Copy to Clipboard Toggle word wrap
1
将这个过滤器限制为只针对 OIDC 令牌端点的请求。
2
检查响应 Content-Type 标头。
3
使用 OidcRequestContextProperties 请求属性确认它是 refresh_grant 令牌授权响应。
4
确认响应 JSON 包含 refresh_token 属性。

OidcResponseFilter 可以通过准备 io.vertx.mutiny.core.buffer.Buffer 的实例来自定义响应正文,并将其设置为响应上下文的属性,例如:

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.OidcEndpoint.Type;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcResponseFilter;
import io.vertx.core.json.JsonObject;
import io.vertx.mutiny.core.buffer.Buffer;

@ApplicationScoped
@Unremovable
@OidcEndpoint(value = Type.TOKEN)
public class TokenResponseFilter implements OidcResponseFilter {

    @Override
    public void filter(OidcResponseContext rc) {
        JsonObject body = rc.responseBody().toJsonObject();
        // JSON `scope` property has multiple values separated by a comma character.
        // It must be replaced with a space character.
        String scope = body.getString("scope");
        body.put("scope", scope.replace(",", " "));
        rc.responseBody(Buffer.buffer(body.toString()));
    }
}
Copy to Clipboard Toggle word wrap

1.4. Quarkus REST 的令牌传播

quarkus-rest-client-oidc-token-propagation 扩展提供了一个 REST 客户端过滤器 io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter,它简化了身份验证信息的传播。此客户端传播当前活动请求中存在的 bearer 令牌,或作为 HTTP Authorization 标头的 Bearer 方案值从 授权代码流机制 获取的令牌。

您可以使用 io.quarkus.oidc.token.propagation.common.AccessTokenorg.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();
}
Copy to Clipboard Toggle word wrap

或者

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();
}
Copy to Clipboard Toggle word wrap

另外,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.rest-client-oidc-token-propagation.exchange-token=true 
1
Copy to Clipboard Toggle word wrap
1
请注意,当 OidcClient 名称使用 io.quarkus.oidc.token.propagation.common.AccessToken"exchangeTokenClient 注解属性设置时,会忽略 exchange -token 配置属性。
注意

AccessTokenRequestReactiveFilter 将使用 OidcClient 来交换当前令牌,您可以使用 quarkus.oidc-client.grant-options.exchange 设置 OpenID Connect 提供程序期望的额外交换属性。

如果您使用需要使用 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.resteasy-client-oidc-token-propagation.exchange-token=true
Copy to Clipboard Toggle word wrap

AccessTokenRequestReactiveFilter 默认使用默认的 OidcClient。可以使用 quarkus.rest-client-oidc-token-propagation.client-name 配置属性或 io.quarkus.oidc.token.propagation.common.AccessToken SerialexchangeTokenClient 注解属性来选择命名的 OidcClient

1.5. RESTEasy Classic 的令牌传播

quarkus-resteasy-client-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

以下小节演示了 AccessTokenRequestFilterJsonWebTokenRequestFilter 可以如何提供帮助。

1.5.1. RestClient AccessTokenRequestFilter

AccessTokenRequestFilter 将所有令牌视为 Strings,因此它可以使用 JWT 和 opaque 令牌。

您可以使用 io.quarkus.oidc.token.propagation.common.AccessTokenorg.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();
}
Copy to Clipboard Toggle word wrap

或者

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();
}
Copy to Clipboard Toggle word wrap

另外,如果 quarkus.resteasy-client-oidc-token-propagation.register-filter 属性设置为 true,并且 quarkus.resteasy-client-oidc-token-propagation.json-web-token 属性设置为 false,则 AccessTokenRequestFilter 属性会自动注册到所有 MP Rest 或 Jakarta REST 客户端。

1.5.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.resteasy-client-oidc-token-propagation.exchange-token=true
Copy to Clipboard Toggle word wrap

如果您使用需要使用 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.resteasy-client-oidc-token-propagation.exchange-token=true
Copy to Clipboard Toggle word wrap
注意

AccessTokenRequestFilter 将使用 OidcClient 来交换当前令牌,您可以使用 quarkus.oidc-client.grant-options.exchange 设置 OpenID Connect 提供程序期望的额外交换属性。

AccessTokenRequestFilter 默认使用 OidcClient。可以使用 quarkus.resteasy-client-oidc-token-propagation.client-name 配置属性来选择命名的 OidcClient

1.5.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.JsonWebTokenorg.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();
}
Copy to Clipboard Toggle word wrap

或者

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();
}
Copy to Clipboard Toggle word wrap

另外,如果 quarkus.resteasy-client-oidc-token-propagation.register-filter 和 quarkus.resteasy-client-oidgation.register-filter 和 quarkus.resteasy-client-oidc-token-propagation.json-web-token 属性都会自动注册 JsonWebTokenRequestFilter

1.5.2.1. 在传播前更新令牌

如果注入的令牌需要有其 iss (issuer)或 aud (audience)声明使用新签名再次更新和保护,则可以配置 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
Copy to Clipboard Toggle word wrap

如前文所述,如果使用 Keycloak 或支持 Token Exchange 协议的 OpenID Connect Provider,请使用 AccessTokenRequestFilter

1.5.3. 测试

通常,您必须准备两个 REST 测试端点。第一个端点使用带有注册的令牌传播过滤器注入的 MP REST 客户端来调用第二个端点。

要了解它如何实现,请特别遵循 OpenID Connect 客户端和令牌传播 快速入门及其 测试 部分。

1.6. 配置参考

1.6.1. OIDC 客户端

build 时修复的 :在构建时修复的配置属性 - 所有其他配置属性在运行时可覆盖

Expand

配置属性

Type

default

在构建时修复了 quarkus.oidc-client.enabled

如果启用了 OIDC 客户端扩展。

环境变量: QUARKUS_OIDC_CLIENT_ENABLED

布尔值

true

quarkus.oidc-client.auth-server-url

quarkus.oidc-client."id".auth-server-url

OpenID Connect (OIDC)服务器的基本 URL,例如 https://host:port/auth。如果您使用 'quarkus-oidc',且只需要公钥验证(公钥)或证书链验证(certificate-chain),则不要设置此属性。默认情况下,OIDC 发现端点会在这个 URL 中附加 .well-known/openid-configuration 路径来调用。对于 Keycloak,使用 https://host:port/realms/{realm},将 {realm} 替换为 Keycloak 域名。

环境变量: QUARKUS_OIDC_CLIENT_AUTH_SERVER_URL

string

 

quarkus.oidc-client.discovery-enabled

quarkus.oidc-client."id".discovery-enabled

发现 OIDC 端点。如果没有启用,您必须单独配置 OIDC 端点 URL。

环境变量: QUARKUS_OIDC_CLIENT_DISCOVERY_ENABLED

布尔值

true

quarkus.oidc-client.registration-path

quarkus.oidc-client."id".registration-path

OIDC 动态客户端注册端点的相对路径或绝对路径或绝对路径 URL。如果 discovery-enabledfalse,或必须自定义发现的令牌端点路径。

环境变量: QUARKUS_OIDC_CLIENT_REGISTRATION_PATH

string

 

quarkus.oidc-client.connection-delay

quarkus.oidc-client."id".connection-delay

尝试初始连接到 OIDC 服务器的持续时间。例如,将持续时间设置为 20S 允许 10 次重试,每 2 秒。此属性仅在创建初始 OIDC 连接时才有效。对于丢弃的连接,请使用 connection-retry-count 属性。

环境变量: QUARKUS_OIDC_CLIENT_CONNECTION_DELAY

持续时间 5-4 Duration 格式

 

quarkus.oidc-client.connection-retry-count

quarkus.oidc-client."id".connection-retry-count

如果临时丢失,重试重新建立现有 OIDC 连接的次数。与 connection-delay 不同,它只适用于初始连接尝试。例如,如果对 OIDC 令牌端点的请求因为连接问题而失败,它将按此设置重试。

环境变量: QUARKUS_OIDC_CLIENT_CONNECTION_RETRY_COUNT

int

3

quarkus.oidc-client.connection-timeout

quarkus.oidc-client."id".connection-timeout

当前 OIDC 连接请求超时的秒数。

环境变量: QUARKUS_OIDC_CLIENT_CONNECTION_TIMEOUT

持续时间 5-4 Duration 格式

10S

quarkus.oidc-client.use-blocking-dns-lookup

quarkus.oidc-client."id".use-blocking-dns-lookup

是否应该对 worker 线程执行 DNS 查找。当您可以看到由 HTTP 请求到 OIDC 服务器阻止的 Vert.x 事件循环的日志记录警告时,请使用这个选项。

环境变量: QUARKUS_OIDC_CLIENT_USE_BLOCKING_DNS_LOOKUP

布尔值

false

quarkus.oidc-client.max-pool-size

quarkus.oidc-client."id".max-pool-size

WebClient 使用的连接池的最大大小。

环境变量: QUARKUS_OIDC_CLIENT_MAX_POOL_SIZE

int

 

quarkus.oidc-client.follow-redirects

quarkus.oidc-client."id".follow-redirects

当 WebClient 获取 HTTP 302 时,自动关注重定向。当此属性只禁用到完全相同原始 URI 的单个重定向时,才允许在重定向请求期间设置了一个或多个 Cookie 时。

环境变量: QUARKUS_OIDC_CLIENT_FOLLOW_REDIRECTS

布尔值

true

quarkus.oidc-client.token-path

quarkus.oidc-client."id".token-path

签发访问和刷新令牌的 OIDC 令牌端点;指定为相对路径或绝对 URL。如果 discovery-enabledfalse,或必须自定义发现的令牌端点路径。

环境变量: QUARKUS_OIDC_CLIENT_TOKEN_PATH

string

 

quarkus.oidc-client.revoke-path

quarkus.oidc-client."id".revoke-path

OIDC 令牌撤销端点的相对路径或绝对路径 URL。

环境变量: QUARKUS_OIDC_CLIENT_REVOKE_PATH

string

 

quarkus.oidc-client.client-id

quarkus.oidc-client."id".client-id

应用程序的客户端 ID。每个应用都有一个客户端 ID,用于标识应用程序。如果 application-typeservice,且不需要令牌内省,则不需要设置客户端 ID。

环境变量: QUARKUS_OIDC_CLIENT_CLIENT_ID

string

 

quarkus.oidc-client.client-name

quarkus.oidc-client."id".client-name

应用的客户端名称。它代表应用程序的人类可读描述,您可以在 OpenId Connect 供应商的仪表板中注册应用程序(客户端)时提供。例如,您可以将此属性设置为具有更详细的日志消息,记录给定客户端的活动。

环境变量: QUARKUS_OIDC_CLIENT_CLIENT_NAME

string

 

quarkus.oidc-client.id

quarkus.oidc-client."id".id

唯一的 OIDC 客户端标识符。它必须在 OIDC 客户端动态创建时设置,并在所有其他情况下是可选的。

环境变量: QUARKUS_OIDC_CLIENT_ID

string

 

quarkus.oidc-client.client-enabled

quarkus.oidc-client."id".client-enabled

如果启用了此客户端配置。

环境变量: QUARKUS_OIDC_CLIENT_CLIENT_ENABLED

布尔值

true

quarkus.oidc-client.scopes

quarkus.oidc-client."id".scopes

访问令牌范围列表

环境变量: QUARKUS_OIDC_CLIENT_SCOPES

字符串列表

 

quarkus.oidc-client.audience

quarkus.oidc-client."id".audience

访问令牌受众列表

环境变量: QUARKUS_OIDC_CLIENT_AUDIENCE

字符串列表

 

quarkus.oidc-client.refresh-token-time-skew

quarkus.oidc-client."id".refresh-token-time-skew

刷新令牌时间偏移。如果启用了此属性,则配置的持续时间转换为秒,并在检查访问令牌是否应刷新时添加到当前时间。如果总和超过此访问令牌的过期时间,则会进行刷新。

环境变量: QUARKUS_OIDC_CLIENT_REFRESH_TOKEN_TIME_SKEW

持续时间 5-4 Duration 格式

 

quarkus.oidc-client.access-token-expires-in

quarkus.oidc-client."id".access-token-expires-in

相对于当前时间的访问令牌到期期限。只有在访问令牌授权响应不包括访问令牌到期属性时,才会检查此属性。

环境变量: QUARKUS_OIDC_CLIENT_ACCESS_TOKEN_EXPIRES_IN

持续时间 5-4 Duration 格式

 

quarkus.oidc-client.access-token-expiry-skew

quarkus.oidc-client."id".access-token-expiry-skew

访问令牌到期时间偏移,可添加到计算的令牌到期时间。

环境变量: QUARKUS_OIDC_CLIENT_ACCESS_TOKEN_EXPIRY_SKEW

持续时间 5-4 Duration 格式

 

quarkus.oidc-client.absolute-expires-in

quarkus.oidc-client."id".absolute-expires-in

如果访问令牌 'expires_in' 属性应检查为绝对时间值,而不是相对于当前时间的持续时间。

环境变量: QUARKUS_OIDC_CLIENT_ABSOLUTE_EXPIRES_IN

布尔值

false

quarkus.oidc-client.grant.type

quarkus.oidc-client."id".grant.type

授权类型

环境变量: QUARKUS_OIDC_CLIENT_GRANT_TYPE

Client: 'client_credentials' 授权只需要 OIDC 客户端身份验证

Password: 'password' 授权需要 OIDC 客户端和用户('username' 和 'password')身份验证

Code: 'authorization_code' 授权需要 OIDC 客户端身份验证,以及至少 'code' 和 'redirect_uri' 参数,它们必须在令牌请求时传递给 OidcClient。

Exchange: 'urn:ietf:params:oauth:grant-type:token-exchange' 授权需要 OIDC 客户端身份验证,以及至少 'subject_token' 参数,该参数必须在令牌请求时传递给 OidcClient。

JWT: 'urn:ietf:params:oauth:grant-type:jwt-bearer' 授权需要 OIDC 客户端身份验证,以及至少一个 'assertion' 参数,该参数必须在令牌请求时传递给 OidcClient。

refresh: 'refresh_token' 授权需要 OIDC 客户端身份验证和刷新令牌。请注意,如果访问令牌获取响应包含刷新令牌,OidcClient 默认支持此授权。然而,在某些情况下,会提供带外的刷新令牌,例如,它可以在多个机密客户端的服务之间共享,等等。如果 'quarkus.oidc-client.grant-type' 设置为 'refresh',则 OidcClient 只支持刷新令牌。

ciba: 'urn:openid:params:grant-type:ciba' 授权需要 OIDC 客户端身份验证,以及 'auth_req_id' 参数,该参数必须在令牌请求时传递给 OidcClient。

device: 'urn:ietf:params:oauth:grant-type:device_code' 授权需要 OIDC 客户端身份验证,以及 'device_code' 参数,该参数必须在令牌请求时传递给 OidcClient。

client

quarkus.oidc-client.grant.access-token-property

quarkus.oidc-client."id".grant.access-token-property

令牌授权响应中的访问令牌属性名称

环境变量: QUARKUS_OIDC_CLIENT_GRANT_ACCESS_TOKEN_PROPERTY

string

access_token

quarkus.oidc-client.grant.refresh-token-property

quarkus.oidc-client."id".grant.refresh-token-property

在令牌授权响应中刷新令牌属性名称

环境变量: QUARKUS_OIDC_CLIENT_GRANT_REFRESH_TOKEN_PROPERTY

string

refresh_token

quarkus.oidc-client.grant.expires-in-property

quarkus.oidc-client."id".grant.expires-in-property

令牌授权响应中的访问令牌到期属性名称

环境变量: QUARKUS_OIDC_CLIENT_GRANT_EXPIRES_IN_PROPERTY

string

expires_in

quarkus.oidc-client.grant.refresh-expires-in-property

quarkus.oidc-client."id".grant.refresh-expires-in-property

在令牌授权响应中刷新令牌到期属性名称

环境变量: QUARKUS_OIDC_CLIENT_GRANT_REFRESH_EXPIRES_IN_PROPERTY

string

refresh_expires_in

quarkus.oidc-client.grant-options."grant-name"

quarkus.oidc-client."id".grant-options."grant-name"

授权选项

环境变量: QUARKUS_OIDC_CLIENT_GRANT_OPTIONS__GRANT_NAME_

Map<String,Map<String,String>>

 

quarkus.oidc-client.early-tokens-acquisition

quarkus.oidc-client."id".early-tokens-acquisition

要求所有使用"OidcClient"的过滤器在后结构初始化时获取令牌,在使用这些令牌之前可能会比较长。如果访问令牌在第一次使用并且没有刷新令牌可用之前过期,则应禁用此属性。

环境变量: QUARKUS_OIDC_CLIENT_EARLY_TOKENS_ACQUISITION

布尔值

true

quarkus.oidc-client.headers."headers"

quarkus.oidc-client."id".headers."headers"

必须发送到令牌端点的自定义 HTTP 标头

环境变量: QUARKUS_OIDC_CLIENT_HEADERS_HEADERS_

Map<String,String>

 

quarkus.oidc-client.refresh-interval

quarkus.oidc-client."id".refresh-interval

令牌刷新间隔。默认情况下,OIDC 客户端会在当前请求期间刷新令牌,当检测到令牌已过期时,如果配置了 refresh-token-time-skew (),则几乎过期。但是,当配置此属性时,OIDC 客户端可以在配置的间隔中异步刷新令牌。此属性只适用于 OIDC 客户端过滤器和其他 AbstractTokensProducer 扩展,但不能在直接使用 OidcClient SerialgetTokens () API 时有效。

环境变量: QUARKUS_OIDC_CLIENT_REFRESH_INTERVAL

持续时间 5-4 Duration 格式

 

HTTP 代理配置

Type

default

quarkus.oidc-client.proxy.host

quarkus.oidc-client."id".proxy.host

代理的主机名或 IP 地址。
注: 如果 OIDC 适配器需要代理与 OIDC 服务器(Provider)通信,请将此值设置为以启用代理的使用。

环境变量: QUARKUS_OIDC_CLIENT_PROXY_HOST

string

 

quarkus.oidc-client.proxy.port

quarkus.oidc-client."id".proxy.port

代理的端口号。默认值为 80

环境变量: QUARKUS_OIDC_CLIENT_PROXY_PORT

int

80

quarkus.oidc-client.proxy.username

quarkus.oidc-client."id".proxy.username

如果代理需要身份验证,则用户名。

环境变量: QUARKUS_OIDC_CLIENT_PROXY_USERNAME

string

 

quarkus.oidc-client.proxy.password

quarkus.oidc-client."id".proxy.password

如果代理需要身份验证,则密码。

环境变量: QUARKUS_OIDC_CLIENT_PROXY_PASSWORD

string

 

TLS configuration

Type

default

quarkus.oidc-client.tls.tls-configuration-name

quarkus.oidc-client."id".tls.tls-configuration-name

要使用的 TLS 配置的名称。

如果配置了名称,它会使用来自 quarkus.tls.<name& gt; configured to name 的配置,但没有找到该名称的 TLS 配置,然后会抛出错误。

默认 TLS 配置 不会被使用

环境变量: QUARKUS_OIDC_CLIENT_TLS_TLS_CONFIGURATION_NAME

string

 

OIDC 客户端用来访问 OIDC 令牌和其他安全端点的不同身份验证选项

Type

default

quarkus.oidc-client.credentials.secret

quarkus.oidc-client."id".credentials.secret

client_secret_basic 身份验证方法使用的客户端 secret。必须设置,除非需要在 client-secretjwt 客户端身份验证中设置 secret。您可以使用 client-secret.value,但这两个属性都是互斥的。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_SECRET

string

 

quarkus.oidc-client.credentials.client-secret.value

quarkus.oidc-client."id".credentials.client-secret.value

客户端机密值。如果设置了 credentials.secret,则忽略这个值。必须设置,除非需要在 client-secretjwt 客户端身份验证中设置 secret。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_CLIENT_SECRET_VALUE

string

 

quarkus.oidc-client.credentials.client-secret.provider.name

quarkus.oidc-client."id".credentials.client-secret.provider.name

CredentialsProvider bean 名称,只有在注册了多个 CredentialsProvider 时才应设置

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_CLIENT_SECRET_PROVIDER_NAME

string

 

quarkus.oidc-client.credentials.client-secret.provider.keyring-name

quarkus.oidc-client."id".credentials.client-secret.provider.keyring-name

CredentialsProvider 密钥环名称。只有在使用的 CredentialsProvider 需要密钥环名称来查找 secret 时,才需要密钥环名称,这通常是当一个 CredentialsProvider 被多个扩展共享时,才需要多个扩展从更多动态源(如 vault 实例或 secret manager)检索凭证时。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_CLIENT_SECRET_PROVIDER_KEYRING_NAME

string

 

quarkus.oidc-client.credentials.client-secret.provider.key

quarkus.oidc-client."id".credentials.client-secret.provider.key

CredentialsProvider 客户端 secret 密钥

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_CLIENT_SECRET_PROVIDER_KEY

string

 

quarkus.oidc-client.credentials.client-secret.method

quarkus.oidc-client."id".credentials.client-secret.method

身份验证方法。如果设置了 clientSecret.value secret,则此方法默认是 基本的

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_CLIENT_SECRET_METHOD

基本:client_secret_basic (默认):客户端 ID 和 secret 使用 HTTP 授权基本方案提交。

post:client_secret_post :客户端 ID 和 secret 作为 client_idclient_secret 表单参数提交。

post-jwt:client_secret_jwt: 客户端 ID 和生成的 JWT secret 作为 client_idclient_secret 形式参数提交。

query :客户端 ID 和 secret 作为 HTTP 查询参数提交。这个选项只支持 OIDC 扩展。

 

quarkus.oidc-client.credentials.jwt.source

quarkus.oidc-client."id".credentials.jwt.source

JWT 令牌源:OIDC 供应商客户端或现有的 JWT bearer 令牌。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_SOURCE

Client: JWT 令牌由 OIDC 供应商客户端生成,以支持 client_secret_jwtprivate_key_jwt 身份验证方法。

bearer: JWT bearer 令牌用作客户端断言 :https://www.rfc-editor.org/rfc/rfc7523#section-2.2。

client

quarkus.oidc-client.credentials.jwt.token-path

quarkus.oidc-client."id".credentials.jwt.token-path

具有 JWT bearer 令牌(应用作客户端断言)的文件路径。此路径只能在 JWT 源(source ())设置为 Source"BEARER 时设置。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_TOKEN_PATH

path

 

quarkus.oidc-client.credentials.jwt.secret

quarkus.oidc-client."id".credentials.jwt.secret

如果提供,则表示 JWT 使用 secret 密钥进行了签名。它与键 , key -filekey-store 属性相互排斥。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_SECRET

string

 

quarkus.oidc-client.credentials.jwt.secret-provider.name

quarkus.oidc-client."id".credentials.jwt.secret-provider.name

CredentialsProvider bean 名称,只有在注册了多个 CredentialsProvider 时才应设置

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_SECRET_PROVIDER_NAME

string

 

quarkus.oidc-client.credentials.jwt.secret-provider.keyring-name

quarkus.oidc-client."id".credentials.jwt.secret-provider.keyring-name

CredentialsProvider 密钥环名称。只有在使用的 CredentialsProvider 需要密钥环名称来查找 secret 时,才需要密钥环名称,这通常是当一个 CredentialsProvider 被多个扩展共享时,才需要多个扩展从更多动态源(如 vault 实例或 secret manager)检索凭证时。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_SECRET_PROVIDER_KEYRING_NAME

string

 

quarkus.oidc-client.credentials.jwt.secret-provider.key

quarkus.oidc-client."id".credentials.jwt.secret-provider.key

CredentialsProvider 客户端 secret 密钥

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_SECRET_PROVIDER_KEY

string

 

quarkus.oidc-client.credentials.jwt.key

quarkus.oidc-client."id".credentials.jwt.key

私钥的字符串表示。如果提供,则表示 JWT 是使用 PEM 或 JWK 格式的私钥签名的。它与 secretkey-filekey-store 属性相互排斥。您可以使用 signature-algorithm 属性覆盖默认的密钥算法 RS256

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_KEY

string

 

quarkus.oidc-client.credentials.jwt.key-file

quarkus.oidc-client."id".credentials.jwt.key-file

如果提供,则表示 JWT 是使用 PEM 或 JWK 格式的私钥签名的。它与 secretkeykey-store 属性相互排斥。您可以使用 signature-algorithm 属性覆盖默认的密钥算法 RS256

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_KEY_FILE

string

 

quarkus.oidc-client.credentials.jwt.key-store-file

quarkus.oidc-client."id".credentials.jwt.key-store-file

如果提供,则表示 JWT 使用密钥存储中的私钥签名。它与 secretkeykey-file 属性相互排斥。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_KEY_STORE_FILE

string

 

quarkus.oidc-client.credentials.jwt.key-store-password

quarkus.oidc-client."id".credentials.jwt.key-store-password

指定密钥存储文件的密码的参数。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_KEY_STORE_PASSWORD

string

 

quarkus.oidc-client.credentials.jwt.key-id

quarkus.oidc-client."id".credentials.jwt.key-id

私钥 ID 或别名。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_KEY_ID

string

 

quarkus.oidc-client.credentials.jwt.key-password

quarkus.oidc-client."id".credentials.jwt.key-password

私钥密码。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_KEY_PASSWORD

string

 

quarkus.oidc-client.credentials.jwt.audience

quarkus.oidc-client."id".credentials.jwt.audience

JWT 受众(ud)声明值。默认情况下,audience 设置为 OpenId Connect Provider 的令牌端点的地址。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_AUDIENCE

string

 

quarkus.oidc-client.credentials.jwt.token-key-id

quarkus.oidc-client."id".credentials.jwt.token-key-id

添加为 JWT kid 标头的签名密钥标识符。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_TOKEN_KEY_ID

string

 

quarkus.oidc-client.credentials.jwt.issuer

quarkus.oidc-client."id".credentials.jwt.issuer

添加为 JWT 的签名密钥的签发者 是s 声明。默认值为客户端 ID。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_ISSUER

string

 

quarkus.oidc-client.credentials.jwt.subject

quarkus.oidc-client."id".credentials.jwt.subject

添加为 JWT 声明的签名密钥的主题。默认值是客户端 ID。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_SUBJECT

string

 

quarkus.oidc-client.credentials.jwt.claims."claim-name"

quarkus.oidc-client."id".credentials.jwt.claims."claim-name"

其他声明.

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_CLAIMS__CLAIM_NAME_

Map<String,String>

 

quarkus.oidc-client.credentials.jwt.signature-algorithm

quarkus.oidc-client."id".credentials.jwt.signature-algorithm

用于 key-file 属性的签名算法。支持的值有: RS256 (默认)、RS384RS512PS256PS384PS512、ES256、ES256ES384、ES512、ES512、ES512、ESS256HS384HS512.

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_SIGNATURE_ALGORITHM

string

 

quarkus.oidc-client.credentials.jwt.lifespan

quarkus.oidc-client."id".credentials.jwt.lifespan

JWT 生命周期(以秒为单位)。这个值添加到发出 JWT 的时间,以计算过期时间。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_LIFESPAN

int

10

quarkus.oidc-client.credentials.jwt.assertion

quarkus.oidc-client."id".credentials.jwt.assertion

如果为 true,则客户端身份验证令牌是 JWT bearer 授权断言。不生成 'client_assertion' 和 'client_assertion_type' 表单属性,而是只生成 'assertion'。这个选项只支持 OIDC 客户端扩展。

环境变量: QUARKUS_OIDC_CLIENT_CREDENTIALS_JWT_ASSERTION

布尔值

false

关于 Duration 格式

要写入持续时间值,请使用标准 java.time.Duration 格式。如需更多信息,请参阅 Duration#parse ()Java API 文档

您还可以使用简化的格式,从数字开始:

  • 如果值只是一个数字,它代表时间(以秒为单位)。
  • 如果值为数字,后跟 ms,代表时间(毫秒)。

在其他情况下,简化的格式被转换为 java.time.Duration 格式以进行解析:

  • 如果该值是一个数字,后跟 hms,则前缀为 PT
  • 如果值为数字,后跟 d,则会以 P 为前缀。

1.6.2. OIDC 令牌传播

build 时修复的 :在构建时修复的配置属性 - 所有其他配置属性在运行时可覆盖

Expand

配置属性

类型

default

在构建时为 quarkus.rest-client-oidc-token-propagation.enabled

如果启用了 OIDC Token Reactive Propagation。

环境变量: QUARKUS_REST_CLIENT_OIDC_TOKEN_PROPAGATION_ENABLED

布尔值

true

在构建时被修复 quarkus.rest-client-oidc-token-propagation.enabled-during-authentication

SecurityIdentity 增强过程中是否启用令牌传播。

例如,您可能需要使用 SecurityIdentityAugmentor 中的 REST 客户端传播当前令牌,以获取 SecurityIdentity 的额外角色。

请注意,这个功能依赖于重复的上下文。有关 Vert.x 重复上下文的更多信息,请参阅本指南

环境变量: QUARKUS_REST_CLIENT_OIDC_TOKEN_PROPAGATION_ENABLED_DURING_AUTHENTICATION

布尔值

false

在构建时被修复 quarkus.rest-client-oidc-token-propagation.exchange-token

使用 "urn:ietf:params:oauth:grant-type:token-exchange" 或 "urn:ietf:params:oauth:grant-type:jwt-bearer" 令牌授权来交换新令牌的当前令牌。

环境变量: QUARKUS_REST_CLIENT_OIDC_TOKEN_PROPAGATION_EXCHANGE_TOKEN

布尔值

false

在构建时为 quarkus.rest-client-oidc-token-propagation.client-name

配置的 OidcClient 的名称。请注意,只有在启用 exchangeToken 属性时,才会使用此属性。

环境变量: QUARKUS_REST_CLIENT_OIDC_TOKEN_PROPAGATION_CLIENT_NAME

string

 

1.7. 参考

了解如何使用 OpenID Connect (OIDC)和带有过滤器的 OAuth2 客户端在应用程序中获取、刷新和传播访问令牌。

有关 Quarkus 中 OIDC 客户端和 令牌传播支持的更多信息,请参阅 OpenID Connect (OIDC)和 OAuth2 客户端和过滤器参考指南

要使用 Bearer Token Authorization 保护应用程序,请参阅 OpenID Connect (OIDC) Bearer 令牌身份验证 指南。

2.1. 先决条件

要完成本指南,您需要:

2.2. 架构

在本例中,应用程序使用两个 Jakarta REST 资源构建,即 FrontendResourceProtectedResource。在这里,Frontend Resource 使用三种方法之一将访问令牌传播到 ProtectedResource

  • 它可以通过在传播令牌前使用 OIDC 客户端过滤器来获取令牌。
  • 它可以使用编程创建的 OIDC 客户端来获取令牌,并通过将其作为 HTTP 授权标头值传递给 REST 客户端方法来传播令牌。
  • 它可以使用 OIDC 令牌传播过滤器来传播传入的访问令牌。

FrontendResource 有八个端点:

  • /frontend/user-name-with-oidc-client-token
  • /frontend/admin-name-with-oidc-client-token
  • /frontend/user-name-with-oidc-client-token-header-param
  • /frontend/admin-name-with-oidc-client-token-header-param
  • /frontend/user-name-with-oidc-client-token-header-param-blocking
  • /frontend/admin-name-with-oidc-client-token-header-param-blocking
  • /frontend/user-name-with-propagated-token
  • /frontend/admin-name-with-propagated-token

/frontend/user-name-with-oidc-client-token/frontend/admin-name-with-oidc-client-token 端点被调用时,Frontend Resource 使用带有 OIDC 客户端过滤器的 REST 客户端来获取和传播访问令牌到 ProtectedResource。当 /frontend/user-name-with-oidc-client-token-header-param/frontend/admin-name-with-oidc-client-token-header-param 端点被调用,Frontend Resource 使用以编程方式创建的 OIDC 客户端来获取和传播访问令牌到 ProtectedResource,方法是将其作为 HTTP Authorization 标头值传递给 REST 客户端方法。当 /frontend/user-name-with-propagated-token/frontend/admin-name-with-propagated-token 端点被调用时,FrontendResource 使用带有 OIDC Token Propagation Filter 的 REST 客户端将当前的传入访问令牌传播到 ProtectedResource

ProtectedResource 有两个端点:

  • /protected/user-name
  • /protected/admin-name

两个端点返回从传入访问令牌中提取的用户名,该令牌从 FrontendResource 传播到 ProtectedResource。这些端点之间的唯一区别在于,只有在当前访问令牌具有用户角色时才允许调用 /protected/ user -name,只有在当前访问令牌具有 admin 角色时才允许调用 /protected/admin-name

2.3. 解决方案

我们建议您按照下一小节中的说明进行操作,并逐步创建应用程序步骤。但是,您可以进入已完成的示例。

克隆 Git 存储库: git clone https://github.com/quarkusio/quarkus-quickstarts.git -b 3.27,或下载 存档

解决方案位于 security-openid-connect-client-quickstart 目录中

2.4. 创建 Maven 项目

首先,您需要一个新项目。使用以下命令创建新项目:

  • 使用 Quarkus CLI:

    quarkus create app org.acme:security-openid-connect-client-quickstart \
        --extension='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest' \
        --no-code
    cd security-openid-connect-client-quickstart
    Copy to Clipboard Toggle word wrap

    要创建 Gradle 项目,请添加-- gradle or --gradle-kotlin-dsl 选项。

    有关如何安装和使用 Quarkus CLI 的更多信息,请参阅 Quarkus CLI 指南。

  • 使用 Maven:

    mvn com.redhat.quarkus.platform:quarkus-maven-plugin:3.27.1:create \
        -DprojectGroupId=org.acme \
        -DprojectArtifactId=security-openid-connect-client-quickstart \
        -Dextensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest' \
        -DnoCode
    cd security-openid-connect-client-quickstart
    Copy to Clipboard Toggle word wrap

    要创建 Gradle 项目,请添加 -DbuildTool=gradle or -DbuildTool=gradle-kotlin-dsl 选项。

对于 Windows 用户:

  • 如果使用 cmd,(不要使用反向斜杠 \ 并将所有内容放在同一行中)
  • 如果使用 Powershell,则双引号中的 wrap -D 参数,如 "-DprojectArtifactId=security-openid-connect-client-quickstart"

它生成 Maven 项目,导入 oidcrest-client-oidc-filterrest-client-oidc-token-propagation,以及 其余 扩展。

如果您已经配置了 Quarkus 项目,您可以在项目基本目录中运行以下命令来将这些扩展添加到项目中:

  • 使用 Quarkus CLI:

    quarkus extension add oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest
    Copy to Clipboard Toggle word wrap
  • 使用 Maven:

    ./mvnw quarkus:add-extension -Dextensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
    Copy to Clipboard Toggle word wrap
  • 使用 Gradle:

    ./gradlew addExtension --extensions='oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest'
    Copy to Clipboard Toggle word wrap

它为构建文件添加以下扩展:

  • 使用 Maven:

    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest-client-oidc-filter</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest-client-oidc-token-propagation</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-rest</artifactId>
    </dependency>
    Copy to Clipboard Toggle word wrap
  • 使用 Gradle:

    implementation("io.quarkus:quarkus-oidc,rest-client-oidc-filter,rest-client-oidc-token-propagation,rest")
    Copy to Clipboard Toggle word wrap

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());
    }
}
Copy to Clipboard Toggle word wrap

ProtectedResource 返回一个来自 userName ()adminName () 方法的名称。名称从当前的 JsonWebToken 中提取。

接下来,添加以下 REST 客户端:

  1. RestClientWithOidcClientFilter,它使用 quarkus-rest-client-oidc-filter 扩展提供的 OIDC 客户端过滤器来获取和传播访问令牌。
  2. RestClientWithTokenHeaderParam,它接受以编程方式创建的 OidcClient 作为 HTTP Authorization 标头值而获取的令牌。
  3. RestClientWithTokenPropagationFilter,它使用 quarkus-rest-client-oidc-token-propagation 扩展提供的 OIDC 令牌传播过滤器来获取和传播访问令牌。

添加 RestClientWithOidcClientFilter 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.inject.RegisterRestClient;

import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;

@RegisterRestClient
@OidcClientFilter 
1

@Path("/")
public interface RestClientWithOidcClientFilter {

    @GET
    @Produces("text/plain")
    @Path("userName")
    Uni<String> getUserName();

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}
Copy to Clipboard Toggle word wrap
1
在 REST 客户端中注册 OIDC 客户端过滤器,以获取和传播令牌。

添加 RestClientWithTokenHeaderParam 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); 
1


    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName(@HeaderParam("Authorization") String authorization); 
2

}
Copy to Clipboard Toggle word wrap
1 2
RestClientWithTokenHeaderParam REST 客户端预期令牌将作为 HTTP Authorization 标头值传递给它。

添加 RestClientWithTokenPropagationFilter 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.inject.RegisterRestClient;

import io.quarkus.oidc.token.propagation.common.AccessToken;

import io.smallrye.mutiny.Uni;

@RegisterRestClient
@AccessToken 
1

@Path("/")
public interface RestClientWithTokenPropagationFilter {

    @GET
    @Produces("text/plain")
    @Path("userName")
    Uni<String> getUserName();

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}
Copy to Clipboard Toggle word wrap
1
使用 REST 客户端注册 OIDC 令牌传播过滤器,以传播传入的已存在的令牌。
重要

不要在同一 REST 客户端中使用 RestClientWithOidcClientFilterRestClientWithTokenPropagationFilter 接口,因为它们可能会发生冲突,从而导致问题。例如,OIDC 客户端过滤器可以从 OIDC 令牌传播过滤器覆盖令牌,或者传播过滤器在 none 可用时尝试传播令牌,则希望 OIDC 客户端过滤器获取新令牌。

另外,添加 OidcClientCreator 以编程方式创建 OIDC 客户端。OidcClientCreator 支持 RestClientWithTokenHeaderParam REST 客户端调用:

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.OidcClients;
import io.quarkus.oidc.client.runtime.OidcClientConfig;
import io.quarkus.oidc.client.runtime.OidcClientConfig.Grant.Type;
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; 
1

    @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);
    }
}
Copy to Clipboard Toggle word wrap
1
OidcClients 可用于检索已初始化的、名为 OIDC 客户端并按需创建新的 OIDC 客户端。

现在,通过添加 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 io.quarkus.oidc.client.Tokens;
import io.quarkus.oidc.client.runtime.TokensHelper;

import org.eclipse.microprofile.rest.client.inject.RestClient;

import io.smallrye.mutiny.Uni;

@Path("/frontend")
public class FrontendResource {
    @Inject
    @RestClient
    RestClientWithOidcClientFilter restClientWithOidcClientFilter; 
1


    @Inject
    @RestClient
    RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter; 
2


    @Inject
    OidcClientCreator oidcClientCreator;
    TokensHelper tokenHelper = new TokensHelper(); 
3

    @Inject
    @RestClient
    RestClientWithTokenHeaderParam restClientWithTokenHeaderParam; 
4


    @GET
    @Path("user-name-with-oidc-client-token")
    @Produces("text/plain")
    public Uni<String> getUserNameWithOidcClientToken() { 
5

        return restClientWithOidcClientFilter.getUserName();
    }

    @GET
    @Path("admin-name-with-oidc-client-token")
    @Produces("text/plain")
    public Uni<String> getAdminNameWithOidcClientToken() { 
6

	return restClientWithOidcClientFilter.getAdminName();
    }

    @GET
    @Path("user-name-with-propagated-token")
    @Produces("text/plain")
    public Uni<String> getUserNameWithPropagatedToken() { 
7

        return restClientWithTokenPropagationFilter.getUserName();
    }

    @GET
    @Path("admin-name-with-propagated-token")
    @Produces("text/plain")
    public Uni<String> getAdminNameWithPropagatedToken() { 
8

        return restClientWithTokenPropagationFilter.getAdminName();
    }

    @GET
    @Path("user-name-with-oidc-client-token-header-param")
    @Produces("text/plain")
    public Uni<String> getUserNameWithOidcClientTokenHeaderParam() { 
9

    	return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
        		.transformToUni(tokens -> restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()));
    }

    @GET
    @Path("admin-name-with-oidc-client-token-header-param")
    @Produces("text/plain")
    public Uni<String> getAdminNameWithOidcClientTokenHeaderParam() { 
10

    	return tokenHelper.getTokens(oidcClientCreator.getOidcClient()).onItem()
        		.transformToUni(tokens -> restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()));
    }

    @GET
    @Path("user-name-with-oidc-client-token-header-param-blocking")
    @Produces("text/plain")
    public String getUserNameWithOidcClientTokenHeaderParamBlocking() { 
11

    	Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely();
        return restClientWithTokenHeaderParam.getUserName("Bearer " + tokens.getAccessToken()).await().indefinitely();
    }

    @GET
    @Path("admin-name-with-oidc-client-token-header-param-blocking")
    @Produces("text/plain")
    public String getAdminNameWithOidcClientTokenHeaderParamBlocking() { 
12

    	Tokens tokens = tokenHelper.getTokens(oidcClientCreator.getOidcClient()).await().indefinitely();
        return restClientWithTokenHeaderParam.getAdminName("Bearer " + tokens.getAccessToken()).await().indefinitely();
    }

}
Copy to Clipboard Toggle word wrap
1 5 6
FrontendResource 使用带有 OIDC 客户端过滤器注入的 RestClientWithOidcClientFilter REST 客户端来获取并传播访问令牌到 ProtectedResource (当 /frontend/user-name-with-oidc-client-token/frontend/admin-name-with-oidc-client-token )。
2 7 8
FrontendResource 使用注入的 RestClientWithTokenPropagationFilter REST 客户端以及 OIDC 令牌传播过滤器,以便在 /frontend/user-name-with-propagated-token/frontend/admin-name-with-propagated-token 被调用时将当前的传入访问令牌传播到 ProtectedResource
4 9 10
FrontendResource 使用以编程方式创建的 OIDC 客户端来获取和传播访问令牌到 ProtectedResource,方法是将其直接传递给注入的 RestClientWithTokenHeaderParam REST 客户端方法作为 HTTP Authorization 标头值,即 /frontend/user-name-with-oidc-client-token-header-param/frontend/admin-name-with-oidc-client-token-header-param
11 12
有时,在向令牌传播令牌之前,可能需要以阻止方式获取令牌。本例演示了如何在这样的情形中获取令牌。
3
当 OIDC 客户端直接使用时,io.quarkus.oidc.client.runtime.TokensHelper 是一个非常有用的工具,没有 OIDC 客户端过滤器。要使用 TokensHelper,请将 OIDC Client 传递给它以获取令牌,并且 TokensHelper 获取令牌,并在需要时以线程安全的方式刷新它们。

最后,添加 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();
	}

}
Copy to Clipboard Toggle word wrap

此例外映射程序仅用于在测试期间验证 ProtectedResource 是否在令牌没有预期的角色时返回 403。如果没有此映射程序,Quarkus REST (以前称为 RESTEasy 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.RestClientWithTokenHeaderParam/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected
Copy to Clipboard Toggle word wrap

上述配置引用 Keycloak,由 ProtectedResource 用于验证传入的访问令牌,并且 OidcClient 使用 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 KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev
Copy to Clipboard Toggle word wrap

{keycloak.version} 设置为 26.3.0 或更高版本。

您可以在 localhost:8180 访问您的 Keycloak 服务器。

admin 用户身份登录,以访问 Keycloak 管理控制台。密码是 admin

导入 realm 配置文件以创建 新域。如需了解更多详细信息,请参阅关于如何创建新域的 Keycloak 文档。https://www.keycloak.org/docs/latest/server_admin/index.html#_create-realm

quarkus 域文件添加 frontend 客户端,以及 aliceadmin 用户。alice 具有 用户角色admin 同时具有 useradmin 角色。

2.8. 以 dev 模式运行应用程序

要在 dev 模式下运行应用程序,请使用:

  • 使用 Quarkus CLI:

    quarkus dev
    Copy to Clipboard Toggle word wrap
  • 使用 Maven:

    ./mvnw quarkus:dev
    Copy to Clipboard Toggle word wrap
  • 使用 Gradle:

    ./gradlew --console=plain quarkusDev
    Copy to Clipboard Toggle word wrap

Keycloak 的 dev Services 启动 Keycloak 容器,并导入 quarkus-realm.json

打开位于 /q/dev-uiDev UI,然后点击 OpenID Connect Dev UI 卡中的 Keycloak 供应商 链接。

提示时,登录到 OpenID Connect Dev UI 提供的 单个页面应用程序

  • admin 用户身份使用密码 admin 登录。此用户同时具有 admin 和 用户角色。

    • 访问 /frontend/user-name-with-propagated-token,它返回 200
    • 访问 /frontend/admin-name-with-propagated-token,它返回 200
  • 注销,然后以 alice 用户身份使用密码 alice 重新登录。此用户具有用户角色

    • 访问 /frontend/user-name-with-propagated-token,它返回 200
    • 访问 /frontend/admin-name-with-propagated-token,它返回 403

您已测试了 FrontendResource 可以从 OpenID Connect Dev UI 传播访问令牌。

2.9. 在 JVM 模式下运行应用程序

在以 dev 模式探索应用后,您可以将其作为标准 Java 应用运行。

首先,编译它:

  • 使用 Quarkus CLI:

    quarkus build
    Copy to Clipboard Toggle word wrap
  • 使用 Maven:

    ./mvnw install
    Copy to Clipboard Toggle word wrap
  • 使用 Gradle:

    ./gradlew build
    Copy to Clipboard Toggle word wrap

然后运行它:

java -jar target/quarkus-app/quarkus-run.jar
Copy to Clipboard Toggle word wrap

2.10. 以原生模式运行应用程序

您可以将此演示编译为原生代码,不需要修改。

这意味着,您不再需要在生产环境中安装 JVM,因为运行时技术包含在生成的二进制中,并优化以最小资源运行。

编译需要更长的时间,因此默认关闭此步骤。要再次构建,请启用 原生 配置集:

  • 使用 Quarkus CLI:

    quarkus build --native
    Copy to Clipboard Toggle word wrap
  • 使用 Maven:

    ./mvnw install -Dnative
    Copy to Clipboard Toggle word wrap
  • 使用 Gradle:

    ./gradlew build -Dquarkus.native.enabled=true
    Copy to Clipboard Toggle word wrap

在稍等片刻后,构建完成后,您可以直接运行原生二进制文件:

./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner
Copy to Clipboard Toggle word wrap

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' \
 )
Copy to Clipboard Toggle word wrap

使用此令牌调用 /frontend/user-name-with-propagated-token。此命令返回 200 状态代码和名称 alice

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

使用相同的令牌调用 /frontend/admin-name-with-propagated-token。与前面的命令不同,这个命令会返回 403,因为 alice 只有一个 用户角色

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

接下来,获取 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' \
 )
Copy to Clipboard Toggle word wrap

使用此令牌调用 /frontend/user-name-with-propagated-token。此命令返回 200 状态代码和名称 admin

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

使用相同的令牌调用 /frontend/admin-name-with-propagated-token。此命令还会返回 200 状态代码和名称 admin,因为 admin 具有 useradmin 角色:

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token
Copy to Clipboard Toggle word wrap

接下来,检查 FrontendResource 方法,它不会传播现有的令牌,而是使用 OidcClient 获取和传播令牌。如上所示,OidcClient 配置为获取 alice 用户的令牌。

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-oidc-client-token
Copy to Clipboard Toggle word wrap

此命令返回 200 状态代码和名称 alice

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-oidc-client-token
Copy to Clipboard Toggle word wrap

与前面的命令不同,此命令会返回 403 状态代码。

接下来,测试以编程方式创建的 OIDC 客户端是否在 reactive 和 imperative (阻塞)模式中同时使用 RestClientWithTokenHeaderParam 正确获取并传播令牌。

调用 /user-name-with-oidc-client-token-header-param。此命令返回 200 状态代码和名称 alice

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param
Copy to Clipboard Toggle word wrap

调用 /admin-name-with-oidc-client-token-header-param。与前面的命令不同,这个命令会返回一个 403 状态代码:

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param
Copy to Clipboard Toggle word wrap

接下来,在阻塞模式下测试使用 OIDC 客户端的端点。

调用 /user-name-with-oidc-client-token-header-param-blocking。此命令返回 200 状态代码和名称 alice

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-oidc-client-token-header-param-blocking
Copy to Clipboard Toggle word wrap

调用 /admin-name-with-oidc-client-token-header-param-blocking。与前面的命令不同,这个命令会返回一个 403 状态代码:

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-oidc-client-token-header-param-blocking
Copy to Clipboard Toggle word wrap

2.12. 参考

法律通告

Copyright © 2025 Red Hat, Inc.
The text of and illustrations in this document are licensed by Red Hat under a Creative Commons Attribution–Share Alike 3.0 Unported license ("CC-BY-SA"). An explanation of CC-BY-SA is available at http://creativecommons.org/licenses/by-sa/3.0/. In accordance with CC-BY-SA, if you distribute this document or an adaptation of it, you must provide the URL for the original version.
Red Hat, as the licensor of this document, waives the right to enforce, and agrees not to assert, Section 4d of CC-BY-SA to the fullest extent permitted by applicable law.
Red Hat, Red Hat Enterprise Linux, the Shadowman logo, the Red Hat logo, JBoss, OpenShift, Fedora, the Infinity logo, and RHCE are trademarks of Red Hat, Inc., registered in the United States and other countries.
Linux® is the registered trademark of Linus Torvalds in the United States and other countries.
Java® is a registered trademark of Oracle and/or its affiliates.
XFS® is a trademark of Silicon Graphics International Corp. or its subsidiaries in the United States and/or other countries.
MySQL® is a registered trademark of MySQL AB in the United States, the European Union and other countries.
Node.js® is an official trademark of Joyent. Red Hat is not formally related to or endorsed by the official Joyent Node.js open source or commercial project.
The OpenStack® Word Mark and OpenStack logo are either registered trademarks/service marks or trademarks/service marks of the OpenStack Foundation, in the United States and other countries and are used with the OpenStack Foundation's permission. We are not affiliated with, endorsed or sponsored by the OpenStack Foundation, or the OpenStack community.
All other trademarks are the property of their respective owners.
返回顶部
Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

通过我们的产品和服务,以及可以信赖的内容,帮助红帽用户创新并实现他们的目标。 了解我们当前的更新.

让开源更具包容性

红帽致力于替换我们的代码、文档和 Web 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

關於紅帽

我们提供强化的解决方案,使企业能够更轻松地跨平台和环境(从核心数据中心到网络边缘)工作。

Theme

© 2025 Red Hat