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


Red Hat build of Quarkus 3.8

Red Hat Customer Content Services

摘要

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

向红帽构建的 Quarkus 文档提供反馈

要报告错误或改进文档,请登录您的红帽 JIRA 帐户并提交问题。如果您没有红帽 JIRA 帐户,系统会提示您创建一个帐户。

流程

  1. 单击以下链接 来创建 ticket
  2. Summary 中输入有关此问题的简单描述。
  3. 提供有关 描述 中问题或增强功能的详细描述。包括一个 URL,以在文档中发生问题。
  4. Submit 创建问题并将其路由到适当的文档团队。

使开源包含更多

红帽致力于替换我们的代码、文档和 Web 属性中有问题的语言。我们从这四个术语开始:master、slave、黑名单和白名单。由于此项工作十分艰巨,这些更改将在即将推出的几个发行版本中逐步实施。详情请查看 CTO Chris Wright 的信息

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

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

这包括以下内容:

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

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

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

1.1. OidcClient

添加以下依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client</artifactId>
</dependency>

quarkus-oidc-client 扩展提供 reactive io.quarkus.oidc.client.OidcClient,它可用于使用 SmallRye Mutiny 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

OidcClient 将发现令牌端点 URL 为 http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens

或者,如果发现端点不可用,或者您希望在发现端点往返中保存,您可以禁用发现并配置令牌端点地址,并带有相对路径值。例如:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.discovery-enabled=false
# Token endpoint: http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens
quarkus.oidc-client.token-path=/protocol/openid-connect/tokens

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

quarkus.oidc-client.token-path=http://localhost:8180/auth/realms/quarkus/protocol/openid-connect/tokens

在这种情况下,不需要设置 quarkus.oidc-client.auth-server-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

client_credentials 授权允许使用 quarkus.oidc-client.grant-options.client.<param-name>=<value > 为令牌请求设置额外的参数。以下是如何使用 audience 参数设置预期的令牌接收者:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
# 'client' is a shortcut for `client_credentials`
quarkus.oidc-client.grant.type=client
quarkus.oidc-client.grant-options.client.audience=https://example.com/api
1.1.2.2. 密码授权

以下是如何将 OidcClient 配置为使用 密码 授权:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice

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

1.1.2.3. 其他授权

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

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

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

如果出于某种原因,可能需要使用 OidcClient 支持 授权代码授权,因此您无法使用 Quarkus OIDC 扩展 来支持授权代码流。如果您实现授权代码流有很好的原因,您可以配置 OidcClient,如下所示:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=code

然后,您可以使用 OidcClient.accessTokens 方法接受额外属性映射,并传递当前 代码和 redirect_uri 参数来交换令牌的授权代码。

OidcClient 还支持 urn:openid:params:grant-type:ciba grant:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=ciba

然后,您可以使用 OidcClient.accessTokens 方法接受额外属性映射,并传递 auth_req_id 参数来交换令牌授权代码。

1.1.2.4. 授权范围

您可能需要请求特定的一组范围与发布的访问令牌关联。使用专用的 quarkus.oidc-client.scopes list 属性,例如: quarkus.oidc-client.scopes=email,phone

1.1.3. 直接使用 OidcClient

一个可以直接使用 OidcClient,如下所示:

import jakarta.inject.PostConstruct;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;

import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.Tokens;

@Path("/service")
public class OidcClientResource {

    @Inject
    OidcClient client;

    volatile Tokens currentTokens;

    @PostConstruct
    public void init() {
        currentTokens = client.getTokens().await().indefinitely();
    }

    @GET
    public String getResponse() {

        Tokens tokens = currentTokens;
        if (tokens.isAccessTokenExpired()) {
            // Add @Blocking method annotation if this code is used with Reactive RestClient
            tokens = client.refreshTokens(tokens.getRefreshToken()).await().indefinitely();
            currentTokens = tokens;
        }
        // Use tokens.getAccessToken() to configure MP RestClient Authorization header/etc
    }
}

1.1.4. 注入令牌

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

import jakarta.inject.PostConstruct;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;

import io.quarkus.oidc.client.Tokens;

@Path("/service")
public class OidcClientResource {

    @Inject Tokens tokens;

    @GET
    public String getResponse() {
        //  Get the access token, which might have been refreshed.
        String accessToken = tokens.getAccessToken();
        // Use the access token to configure MP RestClient Authorization header/etc
    }
}

1.1.5. use OidcClients

io.quarkus.oidc.client.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

在这种情况下,默认客户端使用 client-enabled=false 属性被禁用。jwt-secret 客户端可以通过以下方式访问:

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;

@Path("/clients")
public class OidcClientResource {

    @Inject
    OidcClients clients;

    @GET
    public String getResponse() {
        OidcClient client = clients.getClient("jwt-secret");
        //Use this client to get the token
    }
}
注意

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

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.vertx.ext.web.RoutingContext;

@Path("/clients")
public class OidcClientResource {

    @Inject
    OidcClients clients;
    @Inject
    RoutingContext context;

    @GET
    public String getResponse() {
        String tenantId = context.get("tenantId");
        // named OIDC tenant and client configurations use the same key:
        OidcClient client = clients.getClient(tenantId);
        //Use this client to get the token
    }
}

如果需要,您也可以以编程方式创建新的 OidcClient,如下所示:

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

import io.quarkus.oidc.client.OidcClient;
import io.quarkus.oidc.client.OidcClients;
import io.quarkus.oidc.client.OidcClientConfig;

import io.smallrye.mutiny.Uni;

@Path("/clients")
public class OidcClientResource {

    @Inject
    OidcClients clients;

    @GET
    public String getResponse() {
        OidcClientConfig cfg = new OidcClientConfig();
        cfg.setId("myclient");
        cfg.setAuthServerUrl("http://localhost:8081/auth/realms/quarkus/");
        cfg.setClientId("quarkus");
        cfg.getCredentials().setSecret("secret");
        Uni<OidcClient> client = clients.newClient(cfg);
        //Use this client to get the token
    }
}

1.1.6. 注入名为 OidcClient 和 token

如果有多个配置的 OidcClient 对象,您可以通过额外的 qualifier @Named OidcClient 指定 OidcClient 注入目标,而不是使用 OidcClients

package io.quarkus.oidc.client;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;

@Path("/clients")
public class OidcClientResource {

    @Inject
    @NamedOidcClient("jwt-secret")
    OidcClient client;

    @GET
    public String getResponse() {
        //Use the client to get the token
    }
}

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

@Provider
@Priority(Priorities.AUTHENTICATION)
@RequestScoped
public class OidcClientRequestCustomFilter implements ClientRequestFilter {

    @Inject
    @NamedOidcClient("jwt-secret")
    Tokens tokens;

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
    }
}

1.1.7. 在 RestClient Reactive ClientFilter 中使用 OidcClient

添加以下 Maven 依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client-reactive-filter</artifactId>
</dependency>

请注意,它还将出现 io.quarkus:quarkus-oidc-client

quarkus-oidc-client-reactive-filter 扩展提供 io.quarkus.oidc.client.filter.OidcClientRequestReactiveFilter

它的工作方式与 OidcClientRequestFilter 的效果类似(请参阅 MicroProfile RestClient 客户端过滤器 中的 OidcClient)- 它使用 OidcClient 获取访问令牌,根据需要刷新该访问令牌,并将它设置为 HTTP Authorization Bearer 方案值。区别在于,它与 Reactive RestClient 配合使用,并实施非阻塞客户端过滤器,在获取或刷新令牌时不会阻断当前的 IO 线程。

OidcClientRequestReactiveFilter 延迟初始令牌获取,直到执行前,以避免阻塞 IO 线程。

您可以使用 io.quarkus.oidc.reactive.filter.OidcClientFilter 或 org.eclipse.microprofile. rest.client.annotation.RegisterProvider 注解来选择性地注册 OidcClientRequestReactiveFilter

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;

@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {

    @GET
    Uni<String> getUserName();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;

@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    Uni<String> getUserName();
}

OidcClientRequestReactiveFilter 默认使用默认的 OidcClient。可以使用 quarkus.oidc-client-reactive-filter.client-name 配置属性来选择命名的 OidcClient。您还可以通过设置 @ OidcClient Filter 注释的 value 属性来选择 OidcClient。通过注解设置的客户端名称的优先级高于 quarkus.oidc-client-reactive-filter.client-name 配置属性。例如,假设 这个 jwt-secret 名为 OIDC 客户端声明,您可以引用此客户端,如下所示:

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;
import io.smallrye.mutiny.Uni;

@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {

    @GET
    Uni<String> getUserName();
}

1.1.8. 在 RestClient ClientFilter 中使用 OidcClient

添加以下 Maven 依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-client-filter</artifactId>
</dependency>

请注意,它还将出现 io.quarkus:quarkus-oidc-client

quarkus-oidc-client-filter 扩展提供 io.quarkus.oidc.client.filter.OidcClientRequestFilter Jakarta REST ClientRequestFilter,它使用 OidcClient 获取访问令牌,根据需要刷新它,并将其设置为 HTTP Authorization Bearer 方案值。

默认情况下,此过滤器将获取 OidcClient,以便在初始化时获取第一对访问和刷新令牌。如果访问令牌短且刷新令牌不可用,则令牌获取应该会延迟为 quarkus.oidc-client.early-tokens-acquisition=false

您可以使用 io.quarkus.oidc.client.filter.OidcClientFilterorg.eclipse.microprofile.rest.client.annotation.RegisterProvider 注解来选择性地注册 OidcClientRequestFilter

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;

@RegisterRestClient
@OidcClientFilter
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientRequestFilter;

@RegisterRestClient
@RegisterProvider(OidcClientRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

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

OidcClientRequestFilter 默认使用默认的 OidcClient。可以使用 quarkus.oidc-client-filter.client-name 配置属性来选择命名的 OidcClient。您还可以通过设置 @ OidcClient Filter 注释的 value 属性来选择 OidcClient。通过注解设置的客户端名称的优先级高于 quarkus.oidc-client-filter.client-name 配置属性。例如,假设 这个 jwt-secret 名为 OIDC 客户端声明,您可以引用此客户端,如下所示:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.client.filter.OidcClientFilter;

@RegisterRestClient
@OidcClientFilter("jwt-secret")
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

1.1.9. 使用自定义 RestClient ClientFilter

如果您愿意,您可以使用自己的自定义过滤器并注入 Tokens

import io.quarkus.oidc.client.Tokens;

@Provider
@Priority(Priorities.AUTHENTICATION)
public class OidcClientRequestCustomFilter implements ClientRequestFilter {

    @Inject
    Tokens tokens;

    @Override
    public void filter(ClientRequestContext requestContext) throws IOException {
        requestContext.getHeaders().add(HttpHeaders.AUTHORIZATION, "Bearer " + tokens.getAccessToken());
    }
}

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

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

1.1.10. 刷新访问令牌

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

如果需要刷新访问令牌,但没有提供刷新令牌,则会尝试使用配置的授权(如 client_credentials )获取新令牌。

有些 OpenID Connect 供应商不会在 client_credentials 授权响应中返回刷新令牌。例如,从 Keycloak 12 开始,client_credentials 默认不会返回刷新令牌。供应商也可以限制使用刷新令牌的次数。

1.1.11. 撤销访问令牌

如果您的 OpenId Connect 供应商(如 Keycloak)支持令牌撤销端点,则使用 OidcClient SerialrevokeAccessToken 来撤销当前的访问令牌。吊销端点 URL 与令牌请求 URI 一起发现,也可以使用 quarkus.oidc-client.revoke-path 配置。

如果将此令牌与 REST 客户端搭配使用,或者访问令牌已用于很长时间且您要刷新它,您可能希望撤销访问令牌。

这可以通过使用刷新令牌请求令牌刷新来实现。但是,如果刷新令牌不可用,您可以首先撤销它,然后请求新的访问令牌来刷新它。

1.1.12. OidcClient 身份验证

OidcClient 必须向 OpenID Connect Provider 进行身份验证,以便 client_credentials 和其他授权请求成功。所有 OIDC 客户端身份验证 选项都被支持,例如:

client_secret_basic:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=mysecret

or

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.client-secret.value=mysecret

或者,使用从 CredentialsProvider 检索的 secret:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app

# This key is used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.client-secret.provider.key=mysecret-key
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.client-secret.provider.name=oidc-credentials-provider

client_secret_post:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.client-secret.value=mysecret
quarkus.oidc-client.credentials.client-secret.method=post

client_secret_jwt,签名算法是 HS256

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.secret=AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow

或者,使用从 CredentialsProvider 检索的 secret,签名算法为 HS256

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app

# This is a key that will be used to retrieve a secret from the map of credentials returned from CredentialsProvider
quarkus.oidc-client.credentials.jwt.secret-provider.key=mysecret-key
# Set it only if more than one CredentialsProvider can be registered
quarkus.oidc-client.credentials.jwt.secret-provider.name=oidc-credentials-provider

private_key_jwt 使用 PEM 密钥文件,签名算法为 RS256

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-file=privateKey.pem

private_key_jwt 带有密钥存储文件,签名算法为 RS256

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus/
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.jwt.key-store-file=keystore.jks
quarkus.oidc-client.credentials.jwt.key-store-password=mypassword
quarkus.oidc-client.credentials.jwt.key-password=mykeypassword

# Private key alias inside the keystore
quarkus.oidc-client.credentials.jwt.key-id=mykeyAlias

使用 client_secret_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
1.1.12.2. Apple POST JWT

Apple OpenID Connect Provider 使用 client_secret_post 方法,其中 secret 是使用 private_key_jwt 身份验证方法生成的 JWT,但使用 Apple 帐户特定的签发者和主题属性。

quarkus-oidc-client 支持非标准 client_secret_post_jwt 身份验证方法,它可以配置如下:

quarkus.oidc-client.auth-server-url=${apple.url}
quarkus.oidc-client.client-id=${apple.client-id}
quarkus.oidc-client.credentials.client-secret.method=post-jwt

quarkus.oidc-client.credentials.jwt.key-file=ecPrivateKey.pem
quarkus.oidc-client.credentials.jwt.signature-algorithm=ES256
quarkus.oidc-client.credentials.jwt.subject=${apple.subject}
quarkus.oidc-client.credentials.jwt.issuer=${apple.issuer}
1.1.12.3. 双向 TLS

有些 OpenID Connect 提供者要求客户端作为 mutual TLS (mTLS)身份验证过程的一部分进行身份验证。

quarkus-oidc-client 可以配置如下,以支持 mTLS

quarkus.oidc-client.tls.verification=certificate-validation

# Keystore configuration
quarkus.oidc-client.tls.key-store-file=client-keystore.jks
quarkus.oidc-client.tls.key-store-password=${key-store-password}

# Add more keystore properties if needed:
#quarkus.oidc-client.tls.key-store-alias=keyAlias
#quarkus.oidc-client.tls.key-store-alias-password=keyAliasPassword

# Truststore configuration
quarkus.oidc-client.tls.trust-store-file=client-truststore.jks
quarkus.oidc-client.tls.trust-store-password=${trust-store-password}
# Add more truststore properties if needed:
#quarkus.oidc-client.tls.trust-store-alias=certAlias

1.1.13. 测试

首先,将以下依赖项添加到 test 项目中:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-junit5</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.awaitility</groupId>
    <artifactId>awaitility</artifactId>
    <scope>test</scope>
</dependency>
1.1.13.1. Wiremock

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

<dependency>
    <groupId>org.wiremock</groupId>
    <artifactId>wiremock</artifactId>
    <scope>test</scope>
</dependency>

编写基于 Wiremock 的 QuarkusTestResourceLifecycleManager,例如:

package io.quarkus.it.keycloak;

import static com.github.tomakehurst.wiremock.client.WireMock.matching;
import static com.github.tomakehurst.wiremock.core.WireMockConfiguration.wireMockConfig;

import java.util.HashMap;
import java.util.Map;

import com.github.tomakehurst.wiremock.WireMockServer;
import com.github.tomakehurst.wiremock.client.WireMock;
import com.github.tomakehurst.wiremock.core.Options.ChunkedEncodingPolicy;

import io.quarkus.test.common.QuarkusTestResourceLifecycleManager;

public class KeycloakRealmResourceManager implements QuarkusTestResourceLifecycleManager {
    private WireMockServer server;

    @Override
    public Map<String, String> start() {

        server = new WireMockServer(wireMockConfig().dynamicPort().useChunkedTransferEncoding(ChunkedEncodingPolicy.NEVER));
        server.start();

        server.stubFor(WireMock.post("/tokens")
                .withRequestBody(matching("grant_type=password&username=alice&password=alice"))
                .willReturn(WireMock
                        .aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody(
                                "{\"access_token\":\"access_token_1\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}")));
        server.stubFor(WireMock.post("/tokens")
                .withRequestBody(matching("grant_type=refresh_token&refresh_token=refresh_token_1"))
                .willReturn(WireMock
                        .aResponse()
                        .withHeader("Content-Type", "application/json")
                        .withBody(
                                "{\"access_token\":\"access_token_2\", \"expires_in\":4, \"refresh_token\":\"refresh_token_1\"}")));


        Map<String, String> conf = new HashMap<>();
        conf.put("keycloak.url", server.baseUrl());
        return conf;
    }

    @Override
    public synchronized void stop() {
        if (server != null) {
            server.stop();
            server = null;
        }
    }
}

准备 REST 测试端点。您可以使用注入的 MP REST 客户端和注册的 OidcClient 过滤器测试前端端点,调用下游端点。此端点将令牌回显。例如,请参阅 Quarkus 存储库中的 integration-tests/oidc-client-wiremock

设置 application.properties,例如:

# Use the 'keycloak.url' property set by the test KeycloakRealmResourceManager
quarkus.oidc-client.auth-server-url=${keycloak.url}
quarkus.oidc-client.discovery-enabled=false
quarkus.oidc-client.token-path=/tokens
quarkus.oidc-client.client-id=quarkus-service-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice

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

1.1.13.2. Keycloak

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

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

启用 io.quarkus.oidc.client.runtime.OidcClientImpl TRACE 级别日志记录,以查看令牌获取和刷新错误的更多详情:

quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".level=TRACE
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientImpl".min-level=TRACE

启用 io.quarkus.oidc.client.runtime.OidcClientRecorder TRACE 级别日志记录,以查看 OidcClient 初始化错误的更多详情:

quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".level=TRACE
quarkus.log.category."io.quarkus.oidc.client.runtime.OidcClientRecorder".min-level=TRACE

1.2. OIDC 请求过滤器

您可以通过注册一个或多个 OidcRequestFilter 实现(可以更新或添加新的请求标头)来过滤 Quarkus 向 OIDC 供应商发出的 OIDC 请求。例如,过滤器可以分析请求正文,并将其摘要添加为新的标头值:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;

import io.quarkus.arc.Unremovable;
import io.quarkus.oidc.common.OidcRequestContextProperties;
import io.quarkus.oidc.common.OidcRequestFilter;
import io.vertx.core.http.HttpMethod;
import io.vertx.mutiny.core.buffer.Buffer;
import io.vertx.mutiny.ext.web.client.HttpRequest;

@ApplicationScoped
@Unremovable
public class OidcRequestCustomizer implements OidcRequestFilter {

    @Override
    public void filter(HttpRequest<Buffer> request, Buffer buffer, OidcRequestContextProperties contextProperties) {
        HttpMethod method = request.method();
        String uri = request.uri();
        if (method == HttpMethod.POST && uri.endsWith("/service") && buffer != null) {
            request.putHeader("Digest", calculateDigest(buffer.toString()));
        }
    }

    private String calculateDigest(String bodyString) {
        // Apply the required digest algorithm to the body string
    }
}

1.3. Token Propagation Reactive

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

您可以使用 io.quarkus.oidc.token.propagation.AccessTokenorg.eclipse.microprofile.rest.client.annotation.RegisterProvider 注解来选择性地注册 AccessTokenRequestReactiveFilter,例如:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessToken;

@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;

@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

另外,AccessTokenRequestReactiveFilter 可以支持在传播令牌前交换令牌的复杂应用程序。

如果您使用 Keycloak 或其他支持 Token Exchange 令牌授权的 OIDC 供应商,您可以配置 AccessTokenRequestReactiveFilter 来交换令牌,如下所示:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange

quarkus.oidc-token-propagation.exchange-token=true 1
1
请注意,当 OidcClient 名称使用 io.quarkus.oidc.token.propagation.AccessToken#exchangeTokenClient 注解属性设置时,会忽略 exchange-token 配置属性。

Note AccessTokenRequestReactiveFilter 将使用 OidcClient 来交换当前令牌,您可以使用 quarkus.oidc-client.grant-options.exchange 来设置 OpenID Connect Provider 所需的额外交换属性。

如果您使用需要使用 JWT bearer 令牌 授权的 Azure 等提供程序来交换当前令牌,您可以配置 AccessTokenRequestReactiveFilter 来交换令牌,如下所示:

quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret

quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access

quarkus.oidc-token-propagation-reactive.exchange-token=true

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

1.4. 令牌传播

quarkus-oidc-token-propagation 扩展提供了两个 Jakarta REST jakarta.ws.rs.client.ClientRequestFilter 类实现,以简化身份验证信息的传播。io.quarkus.oidc.token.propagation.AccessTokenRequestFilter 传播在当前活动请求中的 Bearer 令牌,或从 Authorization 代码流机制获取的令牌,作为 HTTP Authorization 标头的 Bearer 方案值。io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter 提供相同的功能,但也提供对 JWT 令牌的支持。

当您需要传播当前的授权代码流访问令牌时,直接令牌传播将正常工作,因为代码流访问令牌(而不是 ID 令牌)旨在为当前的 Quarkus 端点传播,以代表当前经过身份验证的用户访问远程服务。

但是,应避免直接端到端的 Bearer 令牌传播。例如,Client → Service A → Service B,其中 Service B 接收由 Client 发送到 Service A 的令牌。在这种情况下,Service B 无法区分令牌是否来自 Service A 或直接与 客户端。对于 Service B 验证令牌来自 Service A,它应该能够断言新的签发者和受众声明。

此外,复杂的应用可能需要在传播令牌之前交换或更新令牌。例如,当 Service A 访问 Service B 时,访问上下文可能会有所不同。在这种情况下,服务 A 可能会被授予一个范围范围,或者完全不同的范围来访问服务 B

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

1.4.1. RestClient AccessTokenRequestFilter

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

您可以使用 io.quarkus.oidc.token.propagation.AccessTokenorg.eclipse.microprofile.rest.client.annotation.RegisterProvider 来选择性地注册 AccessTokenRequestFilter,例如:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessToken;

@RegisterRestClient
@AccessToken
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.AccessTokenRequestFilter;

@RegisterRestClient
@RegisterProvider(AccessTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

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

1.4.1.1. 在传播前交换令牌

如果需要在传播前交换当前的访问令牌,并使用 Keycloak 或其他支持 Token Exchange 令牌授权的 OpenID Connect 供应商,您可以配置 AccessTokenRequestFilter,如下所示:

quarkus.oidc-client.auth-server-url=http://localhost:8180/auth/realms/quarkus
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret
quarkus.oidc-client.grant.type=exchange
quarkus.oidc-client.grant-options.exchange.audience=quarkus-app-exchange

quarkus.oidc-token-propagation.exchange-token=true

如果您使用需要使用 JWT bearer 令牌 授权的 Azure 等提供程序来交换当前令牌,您可以配置 AccessTokenRequestFilter 以交换令牌,如下所示:

quarkus.oidc-client.auth-server-url=${azure.provider.url}
quarkus.oidc-client.client-id=quarkus-app
quarkus.oidc-client.credentials.secret=secret

quarkus.oidc-client.grant.type=jwt
quarkus.oidc-client.grant-options.jwt.requested_token_use=on_behalf_of
quarkus.oidc-client.scopes=https://graph.microsoft.com/user.read,offline_access

quarkus.oidc-token-propagation.exchange-token=true

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

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

1.4.2. RestClient JsonWebTokenRequestFilter

如果使用 Bearer JWT 令牌,则建议使用 JsonWebTokenRequestFilter,其中这些令牌可以具有其声明,如 签发者使用者 修改,以及再次修改更新的令牌(如重新签名)。它需要一个注入的 org.eclipse.microprofile.jwt.JsonWebToken,因此无法使用不透明令牌。另外,如果您的 OpenID Connect Provider 支持 Token Exchange 协议,则建议使用 AccessTokenRequestFilter,因为 JWT 和不透明 bearer 令牌都可以安全地与 AccessTokenRequestFilter 交换。

JsonWebTokenRequestFilter 可使 Service A 实施轻松更新注入的 org.eclipse.microprofile.jwt.WebToken 及新的 签发者audience 声明值,并使用新的签名再次保护更新的令牌。唯一的难度是确保服务 A 具有签名密钥;它应该从安全文件系统或远程安全存储(如 Vault)置备。

您可以使用 io.quarkus.oidc.token.propagation.JsonWebTokenorg.eclipse.microprofile.rest.client.annotation.RegisterProvider 来选择性地注册 JsonWebTokenRequestFilter,例如:

import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebToken;

@RegisterRestClient
@JsonWebToken
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

or

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
import io.quarkus.oidc.token.propagation.JsonWebTokenRequestFilter;

@RegisterRestClient
@RegisterProvider(JsonWebTokenRequestFilter.class)
@Path("/")
public interface ProtectedResourceService {

    @GET
    String getUserName();
}

另外,如果 quarkus.oidc-token-propagation.register-filterquarkus.oidc-token-propagation.oidc-token-propagation.json-web-token 属性都会自动注册 JsonWebTokenRequestFilter

1.4.2.1. 在传播前更新令牌

如果注入的令牌需要有其 iss (issuer)或 aud (audience)声明使用新签名再次更新和保护,则可以配置 JsonWebTokenRequestFilter,如下所示:

quarkus.oidc-token-propagation.secure-json-web-token=true
smallrye.jwt.sign.key.location=/privateKey.pem
# Set a new issuer
smallrye.jwt.new-token.issuer=http://frontend-resource
# Set a new audience
smallrye.jwt.new-token.audience=http://downstream-resource
# Override the existing token issuer and audience claims if they are already set
smallrye.jwt.new-token.override-matching-claims=true

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

1.4.3. 测试

您可以生成令牌,如 OpenID Connect Bearer Token Integration 测试 部分中所述。准备 REST 测试端点。您可以使用测试前端端点,它使用注入的 MP REST 客户端和注册的令牌传播过滤器,调用下游端点。例如,请参阅 Quarkus 存储库中的 integration-tests/oidc-token-propagation

1.5. Token Propagation Reactive

添加以下 Maven 依赖:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
</dependency>

quarkus-oidc-token-propagation-reactive 扩展提供 io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter,可用于传播当前的 BearerAuthorization Code Flow 访问令牌。

quarkus-oidc-token-propagation-reactive 扩展(与非reactive quarkus-oidc-token-propagation 扩展相反)目前不支持在传播前对令牌进行交换或重新签名。但是,这些功能可能会在以后添加。

1.6. 参考

第 2 章 OpenID Connect 客户端和令牌传播快速入门

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

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

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

2.1. 先决条件

要完成本指南,您需要:

2.2. 架构

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

  • 它可以通过在传播令牌前使用 OIDC 令牌传播重新主动过滤器来获取令牌。
  • 它可以使用 OIDC 令牌传播 Reactive 过滤器传播传入的访问令牌。

FrontendResource 有四个端点:

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

FrontendResource 使用带有 OIDC 令牌传播 Reactive 过滤器的 REST 客户端来获取,并在 /frontend/user-name-with-oidc-client-token/frontend/admin-name-with-oidc-client-token 被调用时将访问令牌传播到 ProtectedResource。另外,FrontendResource 使用带有 OpenID Connect Token Propagation Reactive Filter 的 REST 客户端,在 /frontend/user-name-with-propagated-token/frontend/admin-name-with-propagated-token 被调用时,将当前的传入访问令牌传播到 ProtectedResource

ProtectedResource 有两个端点:

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

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

2.3. 解决方案

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

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

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

2.4. 创建 Maven 项目

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

  • 使用 Quarkus CLI:

    quarkus create app org.acme:security-openid-connect-client-quickstart \
        --extension='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
        --no-code
    cd security-openid-connect-client-quickstart

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

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

  • 使用 Maven:

    mvn io.quarkus.platform:quarkus-maven-plugin:3.8.5:create \
        -DprojectGroupId=org.acme \
        -DprojectArtifactId=security-openid-connect-client-quickstart \
        -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive' \
        -DnoCode
    cd security-openid-connect-client-quickstart

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

对于 Windows 用户:

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

此命令生成 Maven 项目,导入 oidc -client-reactive-filteroidc-token-propagation-reactive-filterresteasy-reactive 扩展。

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

  • 使用 Quarkus CLI:

    quarkus extension add oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive
  • 使用 Maven:

    ./mvnw quarkus:add-extension -Dextensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'
  • 使用 Gradle:

    ./gradlew addExtension --extensions='oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive'

这个命令在构建文件中添加以下扩展:

  • 使用 Maven:

    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc-client-reactive-filter</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-oidc-token-propagation-reactive</artifactId>
    </dependency>
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-resteasy-reactive</artifactId>
    </dependency>
  • 使用 Gradle:

    implementation("io.quarkus:quarkus-oidc,oidc-client-reactive-filter,oidc-token-propagation-reactive,resteasy-reactive")

2.5. 编写应用程序

首先实施 ProtectedResource

package org.acme.security.openid.connect.client;

import jakarta.annotation.security.RolesAllowed;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import io.quarkus.security.Authenticated;
import io.smallrye.mutiny.Uni;

import org.eclipse.microprofile.jwt.JsonWebToken;

@Path("/protected")
@Authenticated
public class ProtectedResource {

    @Inject
    JsonWebToken principal;

    @GET
    @RolesAllowed("user")
    @Produces("text/plain")
    @Path("userName")
    public Uni<String> userName() {
        return Uni.createFrom().item(principal.getName());
    }

    @GET
    @RolesAllowed("admin")
    @Produces("text/plain")
    @Path("adminName")
    public Uni<String> adminName() {
        return Uni.createFrom().item(principal.getName());
    }
}

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

接下来,添加两个 REST 客户端: OidcClientRequestReactiveFilterAccessTokenRequestReactiveFilter,其 FrontendResource 用来调用 ProtectedResource

添加 OidcClientRequestReactiveFilter REST 客户端:

package org.acme.security.openid.connect.client;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.quarkus.oidc.client.reactive.filter.OidcClientRequestReactiveFilter;
import io.smallrye.mutiny.Uni;

@RegisterRestClient
@RegisterProvider(OidcClientRequestReactiveFilter.class)
@Path("/")
public interface RestClientWithOidcClientFilter {

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

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}

RestClientWithOidcClientFilter 接口依赖于 OidcClientRequestReactiveFilter 来获取和传播令牌。

添加 AccessTokenRequestReactiveFilter REST 客户端:

package org.acme.security.openid.connect.client;

import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

import org.eclipse.microprofile.rest.client.annotation.RegisterProvider;
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;

import io.quarkus.oidc.token.propagation.reactive.AccessTokenRequestReactiveFilter;
import io.smallrye.mutiny.Uni;

@RegisterRestClient
@RegisterProvider(AccessTokenRequestReactiveFilter.class)
@Path("/")
public interface RestClientWithTokenPropagationFilter {

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

    @GET
    @Produces("text/plain")
    @Path("adminName")
    Uni<String> getAdminName();
}

RestClientWithTokenPropagationFilter 接口依赖于 AccessTokenRequestReactiveFilter 来传播传入的已存在的令牌。

请注意,RestClientWithOidcClientFilterRestClientWithTokenPropagationFilter 接口都是相同的。这是因为在同一 REST 客户端上组合 OidcClientRequestReactiveFilterAccessTokenRequestReactiveFilter 会导致副作用,因为两个过滤器可能会相互干扰。例如,OidcClientRequestReactiveFilter 可以覆盖 AccessTokenRequestReactiveFilter 传播的令牌,或者 AccessTokenRequestReactiveFilter 可能会在没有令牌可用时被调用,并且 OidcClientRequestReactiveFilter 会改为获取新令牌。

现在,通过添加 FrontendResource 来完成创建应用程序:

package org.acme.security.openid.connect.client;

import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;

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

import io.smallrye.mutiny.Uni;

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

    @Inject
    @RestClient
    RestClientWithTokenPropagationFilter restClientWithTokenPropagationFilter;

    @GET
    @Path("user-name-with-oidc-client-token")
    @Produces("text/plain")
    public Uni<String> getUserNameWithOidcClientToken() {
        return restClientWithOidcClientFilter.getUserName();
    }

    @GET
    @Path("admin-name-with-oidc-client-token")
    @Produces("text/plain")
    public Uni<String> getAdminNameWithOidcClientToken() {
	    return restClientWithOidcClientFilter.getAdminName();
    }

    @GET
    @Path("user-name-with-propagated-token")
    @Produces("text/plain")
    public Uni<String> getUserNameWithPropagatedToken() {
        return restClientWithTokenPropagationFilter.getUserName();
    }

    @GET
    @Path("admin-name-with-propagated-token")
    @Produces("text/plain")
    public Uni<String> getAdminNameWithPropagatedToken() {
        return restClientWithTokenPropagationFilter.getAdminName();
    }
}

FrontendResource 使用带有 OIDC 令牌传播 Reactive 过滤器的 REST 客户端,在 /frontend/user-name-with-oidc-client-token/frontend/admin-name-with-oidc-client-token 被调用时,获取并传播访问令牌到 ProtectedResource。另外,FrontendResource 使用带有 OpenID Connect Token Propagation Reactive Filter 的 REST 客户端在 /frontend/user-name-with-propagated-token/frontend/admin-name-with-propagated-token 被调用时将当前的传入访问令牌传播到 ProtectedResource

最后,添加 Jakarta REST ExceptionMapper

package org.acme.security.openid.connect.client;

import jakarta.ws.rs.core.Response;
import jakarta.ws.rs.ext.ExceptionMapper;
import jakarta.ws.rs.ext.Provider;

import org.jboss.resteasy.reactive.ClientWebApplicationException;

@Provider
public class FrontendExceptionMapper implements ExceptionMapper<ClientWebApplicationException> {

	@Override
	public Response toResponse(ClientWebApplicationException t) {
		return Response.status(t.getResponse().getStatus()).build();
	}

}

此例外映射程序仅用于在测试期间验证 ProtectedResource 是否在令牌没有预期的角色时返回 403。如果没有此映射程序,Easy Reactive 可以正确地将来自 REST 客户端调用的转义转换为 500,以避免泄漏下游资源(如 ProtectedResource )的信息。但是,在测试中,无法断言 500 是由授权异常导致的,而不是一些内部错误。

2.6. 配置应用程序

准备代码,您可以配置应用程序:

# Configure OIDC

%prod.quarkus.oidc.auth-server-url=http://localhost:8180/realms/quarkus
quarkus.oidc.client-id=backend-service
quarkus.oidc.credentials.secret=secret

# Tell Dev Services for Keycloak to import the realm file
# This property is ineffective when running the application in JVM or Native modes but only in dev and test modes.

quarkus.keycloak.devservices.realm-path=quarkus-realm.json

# Configure OIDC Client

quarkus.oidc-client.auth-server-url=${quarkus.oidc.auth-server-url}
quarkus.oidc-client.client-id=${quarkus.oidc.client-id}
quarkus.oidc-client.credentials.secret=${quarkus.oidc.credentials.secret}
quarkus.oidc-client.grant.type=password
quarkus.oidc-client.grant-options.password.username=alice
quarkus.oidc-client.grant-options.password.password=alice

# Configure REST clients

%prod.port=8080
%dev.port=8080
%test.port=8081

org.acme.security.openid.connect.client.RestClientWithOidcClientFilter/mp-rest/url=http://localhost:${port}/protected
org.acme.security.openid.connect.client.RestClientWithTokenPropagationFilter/mp-rest/url=http://localhost:${port}/protected

此配置引用 Keycloak,由 ProtectedResource 用于验证传入的访问令牌,以及 OidcClient 使用密码 授权来获取用户 alice 的令牌。两个 REST 客户端都指向"ProtectedResource"的 HTTP 地址。

注意

%prod. 配置集前缀添加到 quarkus.oidc.auth-server-url 可确保当应用程序以 dev 或 test 模式运行时,为您启动 Dev Services。有关更多信息,请参阅 以 dev 模式运行应用

2.7. 启动并配置 Keycloak 服务器

注意

当您以 dev 或 test 模式运行应用程序时,不要启动 Keycloak 服务器 ; Keycloak 的 Dev Services 启动容器。有关更多信息,请参阅 以 dev 模式运行应用。确保将 realm 配置文件放在 classpath 上,它位于 target/classes 目录中。此放置可确保文件在 dev 模式中自动导入。但是,如果您已构建了一个 完整的解决方案,则不需要将 realm 文件添加到类路径,因为构建过程已完成。

要启动 Keycloak 服务器,您可以使用 Docker 并运行以下命令:

docker run --name keycloak -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin -p 8180:8080 quay.io/keycloak/keycloak:{keycloak.version} start-dev

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

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

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

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

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

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

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

  • 使用 Quarkus CLI:

    quarkus dev
  • 使用 Maven:

    ./mvnw quarkus:dev
  • 使用 Gradle:

    ./gradlew --console=plain quarkusDev

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

打开 /q/dev-ui 中的 Dev UI,再单击 OpenID Connect Dev UI 卡中的 Provider: Keycloak 链接。

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

  • alice 身份登录,密码为 alice此用户具有用户角色

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

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

在这种情况下,您要测试 FrontendResource 可以从 OpenID Connect Dev UI 传播访问令牌。

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

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

首先,编译它:

  • 使用 Quarkus CLI:

    quarkus build
  • 使用 Maven:

    ./mvnw install
  • 使用 Gradle:

    ./gradlew build

然后运行它:

java -jar target/quarkus-app/quarkus-run.jar

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

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

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

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

  • 使用 Quarkus CLI:

    quarkus build --native
  • 使用 Maven:

    ./mvnw install -Dnative
  • 使用 Gradle:

    ./gradlew build -Dquarkus.package.type=native

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

./target/security-openid-connect-quickstart-1.0.0-SNAPSHOT-runner

2.11. 测试应用程序

有关以 dev 模式测试应用程序的更多信息,请参阅前面 在 dev mode 中运行该应用

您可以使用 curl 来测试以 JVM 或原生模式启动的应用程序。

获取 alice 的访问令牌:

export access_token=$(\
    curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \
    --user backend-service:secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=alice&password=alice&grant_type=password' | jq --raw-output '.access_token' \
 )

现在,使用此令牌调用 /frontend/user-name-with-propagated-token/frontend/admin-name-with-propagated-token

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

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

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

相反,此命令会返回 403。请记住,alice 仅具有 用户角色

接下来,获取 admin 的访问令牌:

export access_token=$(\
    curl --insecure -X POST http://localhost:8180/realms/quarkus/protocol/openid-connect/token \
    --user backend-service:secret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=admin&password=admin&grant_type=password' | jq --raw-output '.access_token' \
 )

使用此令牌调用 /frontend/user-name-with-propagated-token

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

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

现在,使用此令牌调用 /frontend/admin-name-with-propagated-token

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-propagated-token \
  -H "Authorization: Bearer "$access_token

此命令还会返回 200 状态代码和名称 admin,因为 admin 具有 useradmin 角色。

现在,检查 FrontendResource 方法,它不会传播现有的令牌,而是使用 OidcClient 获取和传播令牌。如前文所示,OidcClient 配置为获取 alice 用户的令牌,因此:

curl -i -X GET \
  http://localhost:8080/frontend/user-name-with-oidc-client-token

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

curl -i -X GET \
  http://localhost:8080/frontend/admin-name-with-oidc-client-token

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

2.12. 参考

法律通告

Copyright © 2024 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 属性中存在问题的语言。欲了解更多详情,请参阅红帽博客.

關於紅帽

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

© 2024 Red Hat, Inc.