如何通过编程方式为Spring Cloud Gateway配置多Realm的Keycloak
问题描述
我正在构建一个Spring Cloud Gateway,已完成Keycloak安全验证集成且运行正常,但当前通过yml文件手动配置单个Realm,现需改为编程方式实现多Realm的动态配置。以下是当前使用的依赖、启动类代码、Security配置代码及yml配置:
依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-gateway</artifactId> </dependency>
启动类代码
@SpringBootApplication @ComponentScan(basePackages = {"com", "com.b","com.auth","com.security"}) public class APIGatewayApplication { public static void main(String[] args) { SpringApplication.run(APIGatewayApplication.class, args); } @Bean public KeycloakConfigResolver keycloakConfigResolver() { return new PathBasedKeycloakConfigResolver(); } }
Security配置代码
@Configuration public class SecurityConfig { @Bean public SecurityWebFilterChain springSecurityFilterChain (ServerHttpSecurity http) { http .authorizeExchange() .anyExchange() .authenticated() .and() .oauth2Login(); // to redirect to oauth2 login page. return http.build(); } }
当前yml配置
spring: security: oauth2: client: provider: my-keycloak-provider: issuer-uri: http://localhost:8280/auth/realms/Default registration: keycloak-spring-gateway-client: provider: my-keycloak-provider client-id: Default client-secret: 8ZRUH62Pfhfde6uqasD8dfgdhvqWt03K6 authorization-grant-type: authorization_code redirect-uri: '{baseUrl}/app' main: web-application-type: reactive application: name: app cloud: gateway: default-filters: - TokenRelay
实现多Realm动态配置的方案
要实现多Realm的动态配置,核心是通过编程方式动态注册OAuth2客户端和Keycloak Realm配置,替代yml中的静态配置。以下是具体实现步骤:
1. 定义Realm配置实体类
创建实体类存储每个Realm的核心配置信息,后续可从数据库、配置中心等外部源加载:
public class RealmConfig { private String realmName; private String issuerUri; private String clientId; private String clientSecret; private String redirectUri; // 构造函数、getter、setter方法省略 }
2. 动态加载Realm配置
实现配置加载逻辑,模拟从外部源读取多个Realm的配置(实际场景可替换为数据库查询、配置中心拉取等):
@Component public class RealmConfigLoader { public List<RealmConfig> loadAllRealms() { List<RealmConfig> realms = new ArrayList<>(); // 加载默认Realm realms.add(new RealmConfig( "Default", "http://localhost:8280/auth/realms/Default", "Default", "8ZRUH62Pfhfde6uqasD8dfgdhvqWt03K6", "{baseUrl}/app" )); // 加载业务Realm realms.add(new RealmConfig( "Customer", "http://localhost:8280/auth/realms/Customer", "customer-gateway-client", "xYzwU73QgijklmnopQrStuvwxYz12345", "{baseUrl}/customer/app" )); return realms; } }
3. 实现动态客户端注册仓库
自定义ReactiveClientRegistrationRepository,替换yml中的静态客户端注册配置:
@Component public class DynamicClientRegistrationRepository implements ReactiveClientRegistrationRepository { private final Map<String, ClientRegistration> registrations = new ConcurrentHashMap<>(); public DynamicClientRegistrationRepository(RealmConfigLoader configLoader) { List<RealmConfig> realmConfigs = configLoader.loadAllRealms(); for (RealmConfig config : realmConfigs) { // 构建对应Realm的OAuth2客户端注册信息 ClientRegistration registration = ClientRegistration.withRegistrationId("keycloak-" + config.getRealmName()) .clientId(config.getClientId()) .clientSecret(config.getClientSecret()) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri(config.getRedirectUri()) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationUri(config.getIssuerUri() + "/protocol/openid-connect/auth") .tokenUri(config.getIssuerUri() + "/protocol/openid-connect/token") .userInfoUri(config.getIssuerUri() + "/protocol/openid-connect/userinfo") .jwkSetUri(config.getIssuerUri() + "/protocol/openid-connect/certs") .userNameAttributeName(IdTokenClaimNames.SUB) .clientName("Keycloak " + config.getRealmName()) .build(); registrations.put(registration.getRegistrationId(), registration); } } @Override public Mono<ClientRegistration> findByRegistrationId(String registrationId) { return Mono.justOrEmpty(registrations.get(registrationId)); } }
4. 配置授权客户端服务
注入自定义的客户端注册仓库,实现动态授权客户端管理:
@Bean public ReactiveOAuth2AuthorizedClientService authorizedClientService( ReactiveClientRegistrationRepository clientRegistrationRepository) { return new InMemoryReactiveOAuth2AuthorizedClientService(clientRegistrationRepository); }
5. 调整Security配置
修改Security配置,使其使用动态客户端注册信息,并支持多Realm的OAuth2登录:
@Configuration public class SecurityConfig { @Bean public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http, ReactiveClientRegistrationRepository clientRegistrationRepository, ReactiveOAuth2AuthorizedClientService authorizedClientService) { OAuth2LoginSpec oauth2Login = http.oauth2Login() .clientRegistrationRepository(clientRegistrationRepository) .authorizedClientService(authorizedClientService); http .authorizeExchange() .anyExchange().authenticated() .and() .oauth2Login(oauth2Login); return http.build(); } }
6. 自定义PathBasedKeycloakConfigResolver(可选)
如果需要根据请求路径匹配对应Realm,自定义Resolver实现路径到Realm的映射:
public class CustomPathBasedKeycloakConfigResolver extends PathBasedKeycloakConfigResolver { private final RealmConfigLoader configLoader; private final Map<String, KeycloakDeployment> deploymentMap = new ConcurrentHashMap<>(); public CustomPathBasedKeycloakConfigResolver(RealmConfigLoader configLoader) { this.configLoader = configLoader; loadAllRealmDeployments(); } private void loadAllRealmDeployments() { List<RealmConfig> realmConfigs = configLoader.loadAllRealms(); for (RealmConfig config : realmConfigs) { KeycloakDeployment deployment = KeycloakDeploymentBuilder.build(config.getIssuerUri()); deployment.setResourceName(config.getClientId()); deployment.setClientSecret(config.getClientSecret()); deploymentMap.put(config.getRealmName(), deployment); } } @Override public KeycloakDeployment resolve(HttpFacade.Request request) { // 从请求路径提取Realm名称,示例:路径/realm/Customer/api/xxx中提取Customer String path = request.getURI(); String realmName = extractRealmFromPath(path); return deploymentMap.getOrDefault(realmName, deploymentMap.get("Default")); } private String extractRealmFromPath(String path) { String[] segments = path.split("/"); for (int i = 0; i < segments.length; i++) { if ("realm".equals(segments[i]) && i + 1 < segments.length) { return segments[i + 1]; } } return "Default"; } }
然后在启动类中替换原有的KeycloakConfigResolver Bean:
@Bean public KeycloakConfigResolver keycloakConfigResolver(RealmConfigLoader configLoader) { return new CustomPathBasedKeycloakConfigResolver(configLoader); }
7. 清理yml静态配置
移除yml中spring.security.oauth2.client下的provider和registration配置,避免与编程式配置冲突。
内容的提问来源于stack exchange,提问作者user3458271
相关产品推荐
相关产品推荐

