如何用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
相关产品推荐
相关产品推荐

