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

基于Predicate或Filter选择ClientRegistration实现多Keycloak Realm登录

多Keycloak Realm 路由动态认证方案

一、替代方案:自定义AuthenticationRequestResolver动态选择ClientRegistration

这是更简洁的实现路径,无需依赖Gateway Filter传递参数,直接在认证发起阶段根据请求路径匹配对应Realm:

  1. 配置多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
  1. 实现路径匹配的认证请求解析器
    自定义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);
    }
}
  1. YAML配置路径与Realm映射
security:
  oauth2:
    path-to-realm:
      "/api/realm1/**": "keycloak-realm1"
      "/api/realm2/**": "keycloak-realm2"
  1. 配置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前运行:

  1. 自定义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());
        }
    }
}
  1. 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
  1. 自定义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);
    }
}
  1. 配置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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 21:03:11