多租户微服务中如何通过auth_manager自定义Keycloak JWKS端点?
实现多租户自定义JWKS端点方案
核心思路
让auth_manager作为统一代理层,提供一个可动态指定租户Realm的JWKS端点,根据传入的Realm参数转发请求到对应Keycloak Realm的JWKS地址并返回结果。其他微服务只需对接这个统一端点,无需各自实现多租户认证逻辑。
具体实现步骤
1. 设计统一JWKS端点
在auth_manager中暴露路径参数化的接口,示例路径:
GET /auth/jwks/{realm}
{realm}:对应Keycloak的租户Realm名称,由调用方通过路径参数传入(也可改用请求头,根据业务习惯选择)
2. 实现JWKS代理Controller
编写接口接收请求,校验Realm合法性后转发到Keycloak获取JWKS:
@RestController @RequestMapping("/auth/jwks") public class JwksProxyController { private final KeycloakJwksFetcher jwksFetcher; private final TenantRealmMapper realmMapper; // 若租户ID与Realm名称不一致,需此映射组件 public JwksProxyController(KeycloakJwksFetcher jwksFetcher, TenantRealmMapper realmMapper) { this.jwksFetcher = jwksFetcher; this.realmMapper = realmMapper; } @GetMapping("/{tenantId}") public ResponseEntity<String> getJwksByTenant(@PathVariable String tenantId) { // 转换租户ID为对应Realm名称(若租户与Realm一一对应可省略此步) String realm = realmMapper.getRealmByTenantId(tenantId); if (realm == null) { return ResponseEntity.badRequest().body("Invalid tenant ID"); } try { String jwksContent = jwksFetcher.fetchJwks(realm); return ResponseEntity.ok() .contentType(MediaType.APPLICATION_JSON) .body(jwksContent); } catch (Exception e) { // 可返回缓存的旧JWKS作为降级方案 return ResponseEntity.status(HttpStatus.SERVICE_UNAVAILABLE).body("Failed to fetch JWKS"); } } }
3. 封装Keycloak JWKS获取工具类
用WebClient实现请求逻辑,并添加缓存减少对Keycloak的重复请求:
@Component @CacheConfig(cacheNames = "jwksCache") public class KeycloakJwksFetcher { @Value("${keycloak.base-url}") private String keycloakBaseUrl; private final WebClient webClient; public KeycloakJwksFetcher(WebClient.Builder webClientBuilder) { this.webClient = webClientBuilder.build(); } @Cacheable(key = "#realm", sync = true) public String fetchJwks(String realm) { String jwksUrl = String.format("%s/realms/%s/protocol/openid-connect/certs", keycloakBaseUrl, realm); return webClient.get() .uri(jwksUrl) .retrieve() .bodyToMono(String.class) .block(); } }
- 开启Spring Cache:在配置类添加
@EnableCaching,并在配置文件设置缓存过期时间:
spring: cache: caffeine: spec: expireAfterWrite=1h # 按Keycloak密钥更新频率调整
4. 保护JWKS端点安全
该端点仅对内部微服务开放,需添加访问控制:
@Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(auth -> auth .requestMatchers("/auth/jwks/**") .hasIpAddress("10.0.0.0/8") // 限制内部服务网段访问 .anyRequest().authenticated() ); return http.build(); } }
5. 其他资源服务对接配置
在资源服务的Spring Security中,动态指定JWKS地址:
@Configuration public class ResourceServerConfig { @Value("${auth-manager.jwks-url}") // 配置为http://auth-manager/auth/jwks/{tenantId} private String jwksTemplateUrl; @Bean public JwtDecoder jwtDecoder() { // 从请求头获取租户ID替换模板中的占位符 Supplier<String> jwksUrlSupplier = () -> { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); String tenantId = request.getHeader("X-Tenant-ID"); return jwksTemplateUrl.replace("{tenantId}", tenantId); }; return NimbusJwtDecoder.withJwkSetUri(jwksUrlSupplier.get()).build(); } }
关键注意事项
- 租户与Realm映射:若租户ID和Realm名称不统一,需维护映射关系(可存在数据库或配置文件)
- 降级策略:Keycloak不可用时,优先返回缓存的JWKS,避免影响业务
- 缓存失效:若Keycloak手动更新密钥,需提供主动刷新缓存的接口
内容的提问来源于stack exchange,提问作者kikicoder
相关产品推荐
相关产品推荐

