You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何用Key Cloak与Spring Security OAuth2实现多租户Spring Cloud API Gateway安全

基于Keycloak与Spring Security OAuth2实现Spring Cloud API Gateway多租户支持

核心思路

多租户场景下,网关需要动态识别租户身份,并根据租户信息对接对应的Keycloak认证实例/Realm,同时确保下游服务能获取租户标识实现数据隔离。核心是通过网关拦截请求提取租户信息,动态适配OAuth2认证流程,结合Keycloak的多Realm能力实现租户隔离。

具体实现步骤

1. 实现租户识别逻辑

首先要确定从请求中提取租户标识的方式,常见方案:

  • 请求头:比如X-Tenant-ID,前端请求时携带
  • 子域名:如tenant-a.yourdomain.com,解析域名提取租户ID
  • 路径前缀:如/tenant-a/api/**,截取路径前缀作为租户ID

示例:自定义GatewayFilter提取租户ID并存入请求上下文

@Component
public class TenantIdentificationFilter implements GatewayFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 从请求头提取租户ID,可替换为域名/路径解析逻辑
        String tenantId = exchange.getRequest().getHeaders().getFirst("X-Tenant-ID");
        if (tenantId == null) {
            return exchange.getResponse().setComplete(); // 无租户标识直接拒绝
        }
        // 将租户ID存入请求属性,供后续认证逻辑使用
        exchange.getAttributes().put("X-Tenant-ID", tenantId);
        return chain.filter(exchange);
    }
}

2. 配置多租户OAuth2认证

Spring Security OAuth2的Reactive模式下,需要自定义ClientRegistrationRepository和ReactiveOAuth2AuthorizedClientManager,根据租户ID动态加载对应的Keycloak客户端配置:

自定义ClientRegistrationRepository

每个租户对应Keycloak中一个独立的Realm,通过租户ID匹配对应的客户端配置:

@Component
public class TenantClientRegistrationRepository implements ReactiveClientRegistrationRepository {
    // 模拟从配置中心或数据库加载租户-客户端映射,实际可替换为持久化存储
    private final Map<String, ClientRegistration> tenantClientMap = new HashMap<>();

    public TenantClientRegistrationRepository() {
        // 示例:初始化两个租户的Keycloak客户端配置
        tenantClientMap.put("tenant-a", ClientRegistration.withRegistrationId("tenant-a")
                .clientId("gateway-client-a")
                .clientSecret("xxxxxx")
                .authorizationUri("https://your-keycloak.com/auth/realms/tenant-a/protocol/openid-connect/auth")
                .tokenUri("https://your-keycloak.com/auth/realms/tenant-a/protocol/openid-connect/token")
                .userInfoUri("https://your-keycloak.com/auth/realms/tenant-a/protocol/openid-connect/userinfo")
                .redirectUri("{baseUrl}/login/oauth2/code/{registrationId}")
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .scope("openid", "profile", "email")
                .build());

        tenantClientMap.put("tenant-b", ClientRegistration.withRegistrationId("tenant-b")
                // 对应tenant-b Realm的客户端配置
                .build());
    }

    @Override
    public Mono<ClientRegistration> findByRegistrationId(String registrationId) {
        // 这里的registrationId实际为租户ID,需从请求上下文获取,可结合ServerWebExchange
        // 注:实际使用时需通过上下文传递租户ID,此处简化示例
        return Mono.justOrEmpty(tenantClientMap.get(registrationId));
    }
}

自定义ReactiveOAuth2AuthorizedClientManager

确保认证时使用当前租户对应的客户端配置:

@Bean
public ReactiveOAuth2AuthorizedClientManager authorizedClientManager(
        ReactiveClientRegistrationRepository clientRegistrationRepository,
        ReactiveOAuth2AuthorizedClientService authorizedClientService) {

    ReactiveOAuth2AuthorizedClientProvider authorizedClientProvider =
            ReactiveOAuth2AuthorizedClientProviderBuilder.builder()
                    .authorizationCode()
                    .refreshToken()
                    .build();

    DefaultReactiveOAuth2AuthorizedClientManager authorizedClientManager =
            new DefaultReactiveOAuth2AuthorizedClientManager(
                    clientRegistrationRepository, authorizedClientService);
    authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);

    // 自定义客户端ID解析逻辑,从请求上下文获取租户ID作为registrationId
    authorizedClientManager.setContextAttributesMapper(context -> {
        ServerWebExchange exchange = context.get(ServerWebExchange.class);
        String tenantId = exchange.getAttribute("X-Tenant-ID");
        return Mono.just(Collections.singletonMap(
                OAuth2AuthorizedClientManager.CLIENT_REGISTRATION_ID_ATTRIBUTE_NAME, tenantId));
    });

    return authorizedClientManager;
}

3. Keycloak多租户配置

  • 为每个租户创建独立的Realm:在Keycloak控制台中添加Realm,设置唯一的Realm名称(如tenant-a)
  • 为每个Realm配置网关客户端:创建Client,设置Client ID、Client Secret,允许的重定向URI(网关的OAuth2回调地址),启用Authorization Code流
  • 配置租户的用户与角色:每个Realm的用户、角色独立管理,确保租户间数据完全隔离

4. 网关路由与租户传递

在网关路由配置中,确保租户标识传递到下游服务,同时可根据租户ID路由到对应服务实例:

@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
    return builder.routes()
            .route("tenant-a-service", r -> r.path("/tenant-a/**")
                    .filters(f -> f.filter(new TenantIdentificationFilter())
                            .addRequestHeader("X-Tenant-ID", exchange -> exchange.getAttribute("X-Tenant-ID")))
                    .uri("lb://tenant-a-service"))
            .route("tenant-b-service", r -> r.path("/tenant-b/**")
                    .filters(f -> f.filter(new TenantIdentificationFilter())
                            .addRequestHeader("X-Tenant-ID", exchange -> exchange.getAttribute("X-Tenant-ID")))
                    .uri("lb://tenant-b-service"))
            .build();
}

参考资料

  • Spring Cloud Gateway官方文档:Reactive Spring Security OAuth2集成章节
  • Keycloak官方文档:多Realm管理、客户端配置指南
  • Spring Security OAuth2官方文档:自定义ReactiveOAuth2AuthorizedClientManager、ClientRegistrationRepository部分

内容的提问来源于stack exchange,提问作者vishal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.19 07:50:30