第 1 章 OpenID Connect (OIDC)和 OAuth2 客户端和过滤器


您可以使用 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
返回顶部
Red Hat logoGithubredditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

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

让开源更具包容性

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

關於紅帽

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

Theme

© 2025 Red Hat