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

多租户微服务中如何通过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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.13 13:07:39