Spring Boot整合Keycloak多租户动态Realm配置最优方案问询
Keycloak多Realm动态校验实现方案
核心思路
默认的单Realm配置仅支持固定公钥验签,多租户场景下需要从请求的JWT中提取签发Realm标识,动态加载对应Realm的验签配置完成校验,无需修改现有业务逻辑。
实现步骤
1. 调整配置文件
移除application.yml中的固定realm配置,新增允许的租户Realm列表:
keycloak: auth-server-url: https://localhost:8443/auth ssl-required: external resource: app bearer-only: true use-resource-role-mappings: true # 新增允许的租户Realm列表,可根据业务扩展 allowed-tenants: Realm1,Realm2
2. 自定义动态JWT解码器
替换Spring Security默认的单实例JwtDecoder,实现根据JWT签发者动态选择对应Realm的解码器:
import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.oauth2.core.OAuth2AuthenticationException; import org.springframework.security.oauth2.jwt.BadJwtException; import org.springframework.security.oauth2.jwt.Jwt; import org.springframework.security.oauth2.jwt.JwtDecoder; import org.springframework.security.oauth2.jwt.JwtDecoders; import org.springframework.security.web.SecurityFilterChain; import java.util.List; import java.util.concurrent.ConcurrentHashMap; @Configuration @EnableWebSecurity public class MultiTenantSecurityConfig { @Value("${keycloak.auth-server-url}") private String authServerUrl; @Value("${keycloak.allowed-tenants}") private List<String> allowedTenants; // 缓存不同Realm的JwtDecoder,避免重复创建实例 private final ConcurrentHashMap<String, JwtDecoder> realmDecoderCache = new ConcurrentHashMap<>(); @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth.anyRequest().authenticated()) .oauth2ResourceServer(oauth2 -> oauth2 .jwt(jwtConfigurer -> jwtConfigurer.decoder(dynamicJwtDecoder())) ); return http.build(); } private JwtDecoder dynamicJwtDecoder() { return token -> { // 1. 轻量解析JWT payload提取issuer(无需验签,生产环境可替换为JJWT等无验签解析工具降低开销) Jwt tempJwt; try { tempJwt = JwtDecoders.fromOidcIssuerLocation( authServerUrl + "/realms/" + allowedTenants.get(0) ).decode(token); } catch (Exception e) { throw new BadJwtException("JWT格式非法"); } String issuer = tempJwt.getIssuer().toString(); String realm = issuer.substring(issuer.lastIndexOf("/") + 1); // 2. 校验Realm是否在允许的租户列表中 if (!allowedTenants.contains(realm)) { throw new OAuth2AuthenticationException("非法租户Realm:" + realm); } // 3. 缓存中获取或创建对应Realm的解码器 JwtDecoder decoder = realmDecoderCache.computeIfAbsent(realm, r -> JwtDecoders.fromOidcIssuerLocation(authServerUrl + "/realms/" + r) ); return decoder.decode(token); }; } }
注意:生产环境建议使用JJWT等工具做无验签的JWT payload解析,避免临时解码的性能开销,同时可给realmDecoderCache添加过期策略,清理长期不用的Realm解码器实例。
3. 适配权限映射(可选)
如果需要保留原有的Keycloak角色、权限映射逻辑,自定义Converter<Jwt, AbstractAuthenticationToken>实现权限转换即可,逻辑与单Realm场景完全一致。
动态租户扩展(可选)
如果后续需要支持租户动态新增无需重启应用,可将allowed-tenants配置放到配置中心(Nacos/Apollo等),配合缓存清理逻辑即可实现租户热更新。
内容的提问来源于stack exchange,提问作者Lucho82
相关产品推荐
相关产品推荐

