基于Predicate或Filter选择ClientRegistration实现多Keycloak Realm登录
多Keycloak Realm 路由动态认证方案
一、替代方案:自定义AuthenticationRequestResolver动态选择ClientRegistration
这是更简洁的实现路径,无需依赖Gateway Filter传递参数,直接在认证发起阶段根据请求路径匹配对应Realm:
- 配置多ClientRegistration
在application.yml中定义两个Keycloak客户端注册,分别绑定不同Realm:
spring: security: oauth2: client: registration: keycloak-realm1: client-id: realm1-client client-secret: realm1-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: openid,profile keycloak-realm2: client-id: realm2-client client-secret: realm2-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: openid,profile provider: keycloak-realm1: issuer-uri: https://your-keycloak/auth/realms/realm1 keycloak-realm2: issuer-uri: https://your-keycloak/auth/realms/realm2
- 实现路径匹配的认证请求解析器
自定义OAuth2AuthorizationRequestResolver,根据请求URI匹配对应的ClientRegistration ID:
@Component public class PathBasedAuthorizationRequestResolver implements OAuth2AuthorizationRequestResolver { private final OAuth2AuthorizationRequestResolver defaultResolver; private final Map<String, String> pathToRegistrationMap; public PathBasedAuthorizationRequestResolver(OAuth2AuthorizationRequestResolver defaultResolver, @Value("#{${security.oauth2.path-to-realm}}") Map<String, String> pathToRegistrationMap) { this.defaultResolver = defaultResolver; this.pathToRegistrationMap = pathToRegistrationMap; } @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request) { String registrationId = matchRegistrationId(request.getRequestURI()); if (registrationId != null) { request.setAttribute(OAuth2AuthorizationRequestResolver.DEFAULT_REGISTRATION_ID_ATTR_NAME, registrationId); } return defaultResolver.resolve(request); } @Override public OAuth2AuthorizationRequest resolve(HttpServletRequest request, String clientRegistrationId) { return defaultResolver.resolve(request, clientRegistrationId); } private String matchRegistrationId(String uri) { return pathToRegistrationMap.entrySet().stream() .filter(entry -> uri.startsWith(entry.getKey())) .map(Map.Entry::getValue) .findFirst() .orElse(null); } }
- YAML配置路径与Realm映射
security: oauth2: path-to-realm: "/api/realm1/**": "keycloak-realm1" "/api/realm2/**": "keycloak-realm2"
- 配置SecurityFilterChain替换默认解析器
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, PathBasedAuthorizationRequestResolver authRequestResolver) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2Login(oauth2 -> oauth2 .authorizationEndpoint(endpoint -> endpoint.authorizationRequestResolver(authRequestResolver)) ); return http.build(); } }
该方案直接在认证入口阶段完成Realm匹配,完全跳过Spring Security的登录选择页,逻辑更清晰。
二、按你的思路实现:让Gateway Filter在认证前执行
若坚持通过Gateway传递Realm参数,需调整Filter执行顺序,确保其在Spring Security认证Filter前运行:
- 自定义Gateway Pre Filter
编写带参数的Gateway Filter,将Realm ID写入请求属性:
public class RealmRouteFilter implements GatewayFilter { private final String realmId; public RealmRouteFilter(String realmId) { this.realmId = realmId; } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { exchange.getAttributes().put("selected-realm", realmId); return chain.filter(exchange); } // 支持YAML配置的参数类 public static class Config { private String realmId; public String getRealmId() { return realmId; } public void setRealmId(String realmId) { this.realmId = realmId; } } // 过滤器工厂,允许YAML配置 @Component public static class RealmRouteFilterFactory extends AbstractGatewayFilterFactory<Config> { public RealmRouteFilterFactory() { super(Config.class); } @Override public GatewayFilter apply(Config config) { return new RealmRouteFilter(config.getRealmId()); } } }
- YAML配置路由与Filter
设置Filter顺序为-100(低于Spring Security Gateway Filter的默认顺序-1,确保优先执行):
spring: cloud: gateway: routes: - id: realm1-route uri: http://your-service-realm1 predicates: - Path=/api/realm1/** filters: - name: RealmRouteFilter args: realmId: keycloak-realm1 order: -100 - id: realm2-route uri: http://your-service-realm2 predicates: - Path=/api/realm2/** filters: - name: RealmRouteFilter args: realmId: keycloak-realm2 order: -100
- 自定义AuthenticationEntryPoint读取Realm参数
重写认证入口逻辑,根据请求属性跳转对应Realm的认证页面:
@Component public class RealmAwareAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint { @Override public Mono<Void> commence(ServerWebExchange exchange, AuthenticationException authException) { String realmId = exchange.getAttribute("selected-realm"); if (realmId != null) { String authUrl = "/oauth2/authorization/" + realmId; return Mono.fromRunnable(() -> exchange.getResponse().setStatusCode(HttpStatus.SEE_OTHER)) .then(exchange.getResponse().getHeaders().setLocation(URI.create(authUrl))) .then(exchange.getResponse().setComplete()); } return super.commence(exchange, authException); } }
- 配置SecurityFilterChain使用自定义入口
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http, RealmAwareAuthenticationEntryPoint authEntryPoint) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2Login(Customizer.withDefaults()) .exceptionHandling(exceptions -> exceptions.authenticationEntryPoint(authEntryPoint)); return http.build(); } }
该方案通过调整Filter顺序实现认证前参数传递,无需编写RouteLocator代码,保持YAML配置的独立性。
内容的提问来源于stack exchange,提问作者dscham
相关产品推荐
相关产品推荐

