5.10. 租户解析
5.10.1. 租户解析顺序
OIDC 租户按照以下顺序解决:
-
如果主动身份验证被禁用,则首先检查
io.quarkus.oidc.Tenant
注解。 -
使用自定义
TenantConfigResolver
的动态租户解析。 -
使用其中一个选项进行静态租户解析: 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
方法解析名为 a
和 b
的两个租户的租户标识符:
# 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
返回 a
或 b
的租户 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
路径匹配机制与 使用 配置 的授权完全相同。
5.10.4.3. 使用最后一个请求路径片段作为租户 ID
租户标识符的默认解析基于惯例,身份验证请求必须在请求路径的最后片段中包含租户标识符。
以下 application.properties
示例演示了如何配置名为 google
和 github
的两个租户:
# 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/google
或 http://localhost:8080/github
的 UI 选项,而无需添加特定的 /google
或 /github
JAX-RS 资源路径。身份验证完成后,租户标识符也会记录在会话 Cookie 名称中。因此,经过身份验证的用户可以访问安全应用程序区域,而无需将 google
或 github
路径值包含在安全 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}
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; } }
默认配置可能类似如下:
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-a
、tenant-b
和默认租户都用于保护同一端点路径。换句话说,在用户通过 租户
配置进行身份验证后,此用户将无法选择在此用户注销前与 tenant-b
或默认配置进行身份验证,并有会话 Cookie 清除或过期。
多个 OIDC web-app
租户可以保护特定于租户的路径的情况不太常见,而且还需要额外关注。当多个 OIDC web-app
租户(如 tenant-a
、tenant-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; } }
默认租户配置应调整如下:
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; } }