5.10. 租户解析


5.10.1. 租户解析顺序

OIDC 租户按照以下顺序解决:

  1. 如果主动身份验证被禁用,则首先检查 io.quarkus.oidc.Tenant 注解。
  2. 使用自定义 TenantConfigResolver 的动态租户解析。
  3. 使用其中一个选项进行静态租户解析: custom TenantResolver、配置的租户路径和默认到最后一个请求路径片段作为租户 ID。

最后,如果在前面的步骤后没有解析租户 ID,则会选择默认的 OIDC 租户。

如需更多信息,请参阅以下部分:

另外,对于 OIDC web-app 应用程序,状态和会话 Cookie 还提供有关在授权代码流启动时使用上述选项之一解析的租户提示。如需更多信息,请参阅 OIDC web-app 应用程序的租户解析 部分。

5.10.2. 使用注解解析

您可以使用 io.quarkus.oidc.Tenant 注解来解析租户标识符,作为使用 io.quarkus.oidc.TenantResolver 的替代选择。

注意

必须禁用主动 HTTP 身份验证(quarkus.http.auth.proactive=false)才能使它正常工作。如需更多信息,请参阅 主动身份验证 指南。

假设应用程序支持两个 OIDC 租户( hr 和 default 租户),所有带 @Tenant ("hr") 的资源方法和类都通过使用 quarkus.oidc.hr.auth-server-url 配置的 OIDC 供应商进行身份验证。相反,所有其他类和方法仍然通过使用默认 OIDC 供应商进行身份验证。

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

import io.quarkus.oidc.Tenant;
import io.quarkus.security.Authenticated;

@Authenticated
@Path("/api/hello")
public class HelloResource {

    @Tenant("hr") 1
    @GET
    @Produces(MediaType.TEXT_PLAIN)
    public String sayHello() {
        return "Hello!";
    }
}
1
io.quarkus.oidc.Tenant 注解必须放在资源类或资源方法上。
提示

在上例中,使用 @Authenticated 注释强制执行 sayHello 端点的身份验证。

或者,如果您使用 HTTP 安全策略 来保护端点,然后是 @Tenant 注解,您必须延迟此策略的权限检查,如下例所示:

quarkus.http.auth.permission.authenticated.paths=/api/hello
quarkus.http.auth.permission.authenticated.methods=GET
quarkus.http.auth.permission.authenticated.policy=authenticated
quarkus.http.auth.permission.authenticated.applies-to=JAXRS 1
1
告诉 Quarkus 在选择了 @Tenant 注解后运行 HTTP 权限检查。

5.10.3. 动态租户配置解析

如果您需要为需要支持的不同租户进行更多动态配置,且不想与配置文件中的多个条目结束,您可以使用 io.quarkus.oidc.TenantConfigResolver

此接口允许您在运行时动态创建租户配置:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import java.util.function.Supplier;

import io.smallrye.mutiny.Uni;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.TenantConfigResolver;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
        String path = context.request().path();
        String[] parts = path.split("/");

        if (parts.length == 0) {
            //Resolve to default tenant configuration
            return null;
        }

        if ("tenant-c".equals(parts[1])) {
            // Do 'return requestContext.runBlocking(createTenantConfig());'
            // if a blocking call is required to create a tenant config,
            return Uni.createFrom().item(createTenantConfig());
        }

        //Resolve to default tenant configuration
        return null;
    }

    private Supplier<OidcTenantConfig> createTenantConfig() {
        final OidcTenantConfig config = new OidcTenantConfig();

        config.setTenantId("tenant-c");
        config.setAuthServerUrl("http://localhost:8180/realms/tenant-c");
        config.setClientId("multi-tenant-client");
        OidcTenantConfig.Credentials credentials = new OidcTenantConfig.Credentials();

        credentials.setSecret("my-secret");

        config.setCredentials(credentials);

        // Any other setting supported by the quarkus-oidc extension

        return () -> config;
    }
}

此方法返回的 OidcTenantConfig 与从 application.properties 解析 oidc 命名空间配置的方式相同。您可以使用 quarkus-oidc 扩展支持的任何设置来填充它。

如果动态租户解析器返回 null,则下一步会尝试 静态租户配置解析

5.10.4. 静态租户配置解析

当您在 application.properties 文件中设置多个租户配置时,您只需要指定租户标识符的解析方式。要配置租户标识符的解析,请使用以下选项之一:

这些租户解析选项会按照列出的顺序尝试,直到租户 id 被解决为止。如果租户 id 仍然未解析(null),则会选择默认(未命名)租户配置。

5.10.4.1. 使用 TenantResolver解析

以下 application.properties 示例演示了如何使用 TenantResolver 方法解析名为 ab 的两个租户的租户标识符:

# Tenant 'a' configuration
quarkus.oidc.a.auth-server-url=http://localhost:8180/realms/quarkus-a
quarkus.oidc.a.client-id=client-a
quarkus.oidc.a.credentials.secret=client-a-secret

# Tenant 'b' configuration
quarkus.oidc.b.auth-server-url=http://localhost:8180/realms/quarkus-b
quarkus.oidc.b.client-id=client-b
quarkus.oidc.b.credentials.secret=client-b-secret

您可以从 io.quarkus.oidc.TenantResolver 返回 ab 的租户 ID:

import io.quarkus.oidc.TenantResolver;
import io.vertx.ext.web.RoutingContext;

public class CustomTenantResolver implements TenantResolver {

    @Override
    public String resolve(RoutingContext context) {
        String path = context.request().path();
        if (path.endsWith("a")) {
            return "a";
        } else if (path.endsWith("b")) {
            return "b";
        } else {
            // default tenant
            return null;
        }
    }
}

在本例中,最后一个请求路径片段的值是一个租户 id,但如果需要,您可以实施更复杂的租户标识符解析逻辑。

5.10.4.2. 配置租户路径

您可以使用 quarkus.oidc.tenant-paths 配置属性来解析租户标识符,作为使用 io.quarkus.oidc.TenantResolver 的替代选择。以下是如何为上例中使用的 HelloResource 资源的 sayHello 端点选择 hr 租户:

quarkus.oidc.hr.tenant-paths=/api/hello 1
quarkus.oidc.a.tenant-paths=/api/* 2
quarkus.oidc.b.tenant-paths=/*/hello 3
1
相同的 path-matching 规则应用到上例中的 quarkus.http.auth.permission.authenticated.paths=/api/hello 配置属性。
2
放置在路径末尾的通配符代表任意数量的路径片段。但是,该路径小于 /api/hello,因此 hr 租户将用于保护 sayHello 端点。
3
cassandra /hello 中的通配符表示一个路径段。然而,通配符小于 api,因此将使用 hr 租户。
提示

路径匹配机制与 使用 配置 的授权完全相同。

5.10.4.3. 使用最后一个请求路径片段作为租户 ID

租户标识符的默认解析基于惯例,身份验证请求必须在请求路径的最后片段中包含租户标识符。

以下 application.properties 示例演示了如何配置名为 googlegithub 的两个租户:

# Tenant 'google' configuration
quarkus.oidc.google.provider=google
quarkus.oidc.google.client-id=${google-client-id}
quarkus.oidc.google.credentials.secret=${google-client-secret}
quarkus.oidc.google.authentication.redirect-path=/signed-in

# Tenant 'github' configuration
quarkus.oidc.github.provider=github
quarkus.oidc.github.client-id=${github-client-id}
quarkus.oidc.github.credentials.secret=${github-client-secret}
quarkus.oidc.github.authentication.redirect-path=/signed-in

在提供的示例中,两个租户都将 OIDC web-app 应用程序配置为使用授权代码流来验证用户,并在身份验证后生成会话 Cookie。在 Google 或 GitHub 验证当前用户后,用户将返回到经过身份验证的用户的 /signed-in 区域,如 JAX-RS 端点上的安全资源路径。

最后,要完成默认租户解析,请设置以下配置属性:

quarkus.http.auth.permission.login.paths=/google,/github
quarkus.http.auth.permission.login.policy=authenticated

如果端点在 http://localhost:8080 上运行,您也可以为用户提供登录到 http://localhost:8080/googlehttp://localhost:8080/github 的 UI 选项,而无需添加特定的 /google/github JAX-RS 资源路径。身份验证完成后,租户标识符也会记录在会话 Cookie 名称中。因此,经过身份验证的用户可以访问安全应用程序区域,而无需将 googlegithub 路径值包含在安全 URL 中。

默认解析也可以用于 Bearer 令牌身份验证。仍然可能不太实际,因为租户标识符必须始终设置为最后一个路径片段值。

5.10.4.4. 使用令牌签发者声明解析租户

支持 Bearer 令牌身份验证的 OIDC 租户可以使用访问令牌的签发者解决。对于基于签发者的解析功能可以正常工作,必须满足以下条件:

  • 访问令牌必须采用 JWT 格式,并且包含签发者(is)令牌声明。
  • 只有带有应用程序类型 服务或 混合的 OIDC 租户才被考虑。这些租户必须发现或配置了令牌签发者。

基于签发者的解析通过 quarkus.oidc.resolve-tenants-with-issuer 属性启用。例如:

quarkus.oidc.resolve-tenants-with-issuer=true 1

quarkus.oidc.tenant-a.auth-server-url=${tenant-a-oidc-provider} 2
quarkus.oidc.tenant-a.client-id=${tenant-a-client-id}
quarkus.oidc.tenant-a.credentials.secret=${tenant-a-client-secret}

quarkus.oidc.tenant-b.auth-server-url=${tenant-b-oidc-provider} 3
quarkus.oidc.tenant-b.discover-enabled=false
quarkus.oidc.tenant-b.token.issuer=${tenant-b-oidc-provider}/issuer
quarkus.oidc.tenant-b.jwks-path=/jwks
quarkus.oidc.tenant-b.token-path=/tokens
quarkus.oidc.tenant-b.client-id=${tenant-b-client-id}
quarkus.oidc.tenant-b.credentials.secret=${tenant-b-client-secret}
1
租户 tenant-atenant-b 使用 JWT 访问令牌的签发者 是声明 值来解决。
2
tenant-a 从 OIDC 供应商已知的配置端点发现 签发者
3
tenant-b 配置 签发者,因为其 OIDC 供应商不支持发现。

5.10.5. OIDC web-app 应用程序的租户解析

OIDC web-app 应用程序的租户解析必须在授权代码流中至少 3 次进行,当 OIDC 租户特定配置会影响以下步骤的运行方式。

第 1 步:未验证的用户访问端点,并重定向到 OIDC 供应商

当未经身份验证的用户访问安全路径时,用户会被重定向到 OIDC 供应商以进行身份验证,并且使用租户配置来构建重定向 URI。

静态租户 配置 解析部分中列出的所有 静态和动态租户解析 选项都可用于解析租户。

第 2 步:用户重定向到端点

在提供程序身份验证后,用户会被重定向到 Quarkus 端点,租户配置用于完成授权代码流。

静态租户 配置 解析部分中列出的所有 静态和动态租户解析 选项都可用于解析租户。在租户解析开始前,授权代码流状态 cookie 用于将已解析的租户配置 id 设置为 RoutingContext tenant-id 属性:自定义 dynamic TenantConfigResolver 和 static TenantResolver 租户解析器都可以检查它。

第 3 步:经过身份验证的用户使用会话 Cookie 访问安全路径

租户配置决定了会话 Cookie 如何验证和刷新。在租户解析开始前,授权代码流 会话 Cookie 用于将已解析的租户配置 id 设为 RoutingContext tenant-id 属性:自定义 dynamic TenantConfigResolver 和 static TenantResolver 租户解析器都可以检查它。

例如,以下是自定义 TenantConfigResolver 如何避免创建已解析的租户配置,否则可能需要阻止对数据库或其他远程源的读取:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
        String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
        if (resolvedTenantId != null) { 1
            return null;
        }

        String path = context.request().path(); 2
        if (path.endsWith("tenant-a")) {
            return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a"));
        } else if (path.endsWith("tenant-b")) {
            return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b"));
        }

        // Default tenant id
        return null;
    }

    private OidcTenantConfig createTenantConfig(String tenantId, String clientId, String secret) {
        final OidcTenantConfig config = new OidcTenantConfig();
        config.setTenantId(tenantId);
        config.setAuthServerUrl("http://localhost:8180/realms/"  + tenantId);
        config.setClientId(clientId);
        config.getCredentials().setSecret(secret);
        config.setApplicationType(ApplicationType.WEB_APP);
        return config;
    }
}
1
如果之前已解析的租户配置,请让 Quarkus 使用已经解析的租户配置。
2
检查创建租户配置的请求路径。

默认配置可能类似如下:

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

上例假定 tenant-atenant-b 和默认租户都用于保护同一端点路径。换句话说,在用户通过 租户 配置进行身份验证后,此用户将无法选择在此用户注销前与 tenant-b 或默认配置进行身份验证,并有会话 Cookie 清除或过期。

多个 OIDC web-app 租户可以保护特定于租户的路径的情况不太常见,而且还需要额外关注。当多个 OIDC web-app 租户(如 tenant-atenant-b 和默认租户)用于控制对租户特定路径的访问时,使用一个 OIDC 供应商进行身份验证的用户必须能够访问需要与另一个供应商进行身份验证的路径,否则可能会导致意外的身份验证失败。例如,如果 tenant-a 身份验证需要 Keycloak 身份验证,并且 tenant-b 身份验证需要 Auth0 身份验证 ,那么如果租户 验证的用户试图访问由 tenant-b 配置保护的路径,则不会验证会话 Cookie,因为 Auth0 公钥无法验证由 Keycloak 签名的令牌。避免多个 web-app 租户冲突的一个简单方法是设置特定于租户的会话路径,如下例所示:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {
        String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
        if (resolvedTenantId != null) { 1
            return null;
        }

        String path = context.request().path(); 2
        if (path.endsWith("tenant-a")) {
            return Uni.createFrom().item(createTenantConfig("tenant-a", "/tenant-a", "client-a", "secret-a"));
        } else if (path.endsWith("tenant-b")) {
            return Uni.createFrom().item(createTenantConfig("tenant-b", "/tenant-b", "client-b", "secret-b"));
        }

        // Default tenant id
        return null;
    }

    private OidcTenantConfig createTenantConfig(String tenantId, String cookiePath, String clientId, String secret) {
        final OidcTenantConfig config = new OidcTenantConfig();
        config.setTenantId(tenantId);
        config.setAuthServerUrl("http://localhost:8180/realms/"  + tenantId);
        config.setClientId(clientId);
        config.getCredentials().setSecret(secret);
        config.setApplicationType(ApplicationType.WEB_APP);
        config.getAuthentication().setCookiePath(cookiePath); 3
        return config;
    }
}
1
如果之前已解析的租户配置,请让 Quarkus 使用已经解析的租户配置。
2
检查创建租户配置的请求路径。
3
设置特定于租户的 Cookie 路径,以确保会话 Cookie 仅对创建它的租户可见。

默认租户配置应调整如下:

quarkus.oidc.auth-server-url=http://localhost:8180/realms/default
quarkus.oidc.client-id=client-default
quarkus.oidc.credentials.secret=secret-default
quarkus.oidc.authentication.cookie-path=/default
quarkus.oidc.application-type=web-app

当多个 OIDC web-app 租户保护租户保护租户时,不建议使用同一会话 Cookie 路径,并应避免它,因为它需要更关注自定义解析器,例如:

package io.quarkus.it.keycloak;

import jakarta.enterprise.context.ApplicationScoped;
import io.quarkus.oidc.OidcRequestContext;
import io.quarkus.oidc.OidcTenantConfig;
import io.quarkus.oidc.OidcTenantConfig.ApplicationType;
import io.quarkus.oidc.TenantConfigResolver;
import io.quarkus.oidc.runtime.OidcUtils;
import io.smallrye.mutiny.Uni;
import io.vertx.ext.web.RoutingContext;

@ApplicationScoped
public class CustomTenantConfigResolver implements TenantConfigResolver {

    @Override
    public Uni<OidcTenantConfig> resolve(RoutingContext context, OidcRequestContext<OidcTenantConfig> requestContext) {

        String path = context.request().path(); 1
        if (path.endsWith("tenant-a")) {
            String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
	    if (resolvedTenantId != null) {
	        if ("tenant-a".equals(resolvedTenantId)) { 2
	            return null;
	        } else {
	           // Require a "tenant-a" authentication
                   context.remove(OidcUtils.TENANT_ID_ATTRIBUTE); 3
	        }
            }
            return Uni.createFrom().item(createTenantConfig("tenant-a", "client-a", "secret-a"));
        } else if (path.endsWith("tenant-b")) {
            String resolvedTenantId = context.get(OidcUtils.TENANT_ID_ATTRIBUTE);
	    if (resolvedTenantId != null) {
	        if ("tenant-b".equals(resolvedTenantId)) { 4
	            return null;
	        } else {
	            // Require a "tenant-b" authentication
                   context.remove(OidcUtils.TENANT_ID_ATTRIBUTE); 5
	        }
            }
            return Uni.createFrom().item(createTenantConfig("tenant-b", "client-b", "secret-b"));
        }

        // Set default tenant id
        context.put(OidcUtils.TENANT_ID_ATTRIBUTE, OidcUtils.DEFAULT_TENANT_ID); 6
        return null;
    }

    private OidcTenantConfig createTenantConfig(String tenantId, String clientId, String secret) {
        final OidcTenantConfig config = new OidcTenantConfig();
        config.setTenantId(tenantId);
        config.setAuthServerUrl("http://localhost:8180/realms/"  + tenantId);
        config.setClientId(clientId);
        config.getCredentials().setSecret(secret);
        config.setApplicationType(ApplicationType.WEB_APP);
        return config;
    }
}
1
检查创建租户配置的请求路径。
2 4
如果当前路径中已解析的租户,则让 Quarkus 使用已经解析的租户配置。
3 5
如果当前路径没有期望解析的租户配置,请删除 tenant-id 属性。
6
对所有其他路径使用默认租户。它等同于删除 tenant-id 属性。
Red Hat logoGithubRedditYoutubeTwitter

学习

尝试、购买和销售

社区

关于红帽文档

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

让开源更具包容性

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

關於紅帽

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

© 2024 Red Hat, Inc.