Spring Security OAuth2 资源服务器配置多认证源怎么操作?

文章导读
Spring Security 默认不支持通过配置文件直接指定多个认证源,需要在代码层面自定义 JwtDecoder 或 AuthenticationManager 来根据 token 中的发行者信息动态选择验证逻辑。
📋 目录
  1. 依赖与配置准备
  2. 核心实现:多源路由 Decoder
  3. 配置 SecurityFilterChain
  4. 怎么验证是否生效
  5. 常见坑与风险规避
A A

Spring Security 默认不支持通过配置文件直接指定多个认证源,需要在代码层面自定义 JwtDecoder 或 AuthenticationManager 来根据 token 中的发行者信息动态选择验证逻辑。

先说结论:这属于高级定制场景,官方没有现成的配置项,需开发介入实现多 Issuer 路由。

  • 适合:多租户系统、联邦身份认证、合并多个微服务认证域
  • 先准备:收集所有认证源的 JWK Set URI 或公钥信息
  • 验收:分别使用不同认证源颁发的 token 进行接口调用测试

依赖与配置准备

确保项目中包含 Spring Security OAuth2 资源服务器依赖。以下是 Maven 配置示例:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>

application.yml 中自定义配置多认证源信息,避免硬编码:

custom-auth-sources:
  - issuer: https://auth-a.com
    jwk-set-uri: https://auth-a.com/.well-known/jwks.json
  - issuer: https://auth-b.com
    jwk-set-uri: https://auth-b.com/.well-known/jwks.json

创建配置属性类以便读取上述配置:

@Data
@ConfigurationProperties(prefix = "custom-auth-sources")
public class AuthSourceProperties {
    private List<AuthSource> sources = new ArrayList<>();

    @Data
    public static class AuthSource {
        private String issuer;
        private String jwkSetUri;
    }
}

核心实现:多源路由 Decoder

不要试图在 application.yml 中罗列多个 issuer-uri,这不会生效。核心思路是拦截请求,先解析 token 头部获取 iss claim,再根据 iss 值选择对应的 JwtDecoder 进行验证。

创建 MultiSourceJwtDecoder 实现类。注意:解析 token 获取 iss 时不进行签名验证,仅用于路由,后续会由具体的 Decoder 进行严格验证。

Spring Security OAuth2 资源服务器配置多认证源怎么操作?
@Component
public class MultiSourceJwtDecoder implements JwtDecoder {

    private final Map<String, JwtDecoder> decoderCache = new ConcurrentHashMap<>();
    private final List<AuthSourceProperties.AuthSource> authSources;

    public MultiSourceJwtDecoder(AuthSourceProperties properties) {
        this.authSources = properties.getSources();
    }

    @Override
    public Jwt decode(String token) throws JwtException {
        // 1. 不验证签名解析 payload 获取 iss (仅用于路由,不可信)
        String issuer = extractIssuer(token);
        
        // 2. 根据 iss 获取或创建对应的 Decoder
        JwtDecoder targetDecoder = decoderCache.computeIfAbsent(issuer, this::createDecoder);
        
        if (targetDecoder == null) {
            throw new JwtException("Unknown issuer: " + issuer);
        }

        // 3. 使用对应的 Decoder 进行正式验证(包含签名校验)
        return targetDecoder.decode(token);
    }

    private String extractIssuer(String token) {
        try {
            // 使用 Nimbus 库解析 claims 但不验证签名
            JwtParserBuilder builder = NimbusJwtDecoder.jwtParserBuilder();
            Jwt<ClaimSet> jwt = builder.build().parseClaimsJwt(token);
            return jwt.getClaims().getClaim("iss");
        } catch (Exception e) {
            throw new JwtException("Invalid token structure", e);
        }
    }

    private JwtDecoder createDecoder(String issuer) {
        Optional<AuthSourceProperties.AuthSource> sourceOpt = authSources.stream()
            .filter(s -> s.getIssuer().equals(issuer))
            .findFirst();

        if (sourceOpt.isPresent()) {
            // NimbusJwtDecoder 内部已处理 JWK 缓存和轮换
            return NimbusJwtDecoder.withJwkSetUri(sourceOpt.get().getJwkSetUri()).build();
        }
        return null;
    }
}

配置 SecurityFilterChain

在安全配置类中,将自定义的 Decoder 注入到 oauth2ResourceServer 配置中:

@Bean
public SecurityFilterChain filterChain(HttpSecurity http, JwtDecoder multiSourceDecoder) throws Exception {
    http
        .oauth2ResourceServer(oauth2 -> oauth2
            .jwt(jwt -> jwt.decoder(multiSourceDecoder))
        );
    return http.build();
}

回滚提醒:如果自定义逻辑复杂,建议保留默认单源配置作为备用分支,以便紧急情况下快速切换。

怎么验证是否生效

1. 日志检查:开启 Spring Security 调试日志,观察请求处理时是否加载了对应 issuer 的公钥。
2. 接口测试:分别获取认证源 A 和认证源 B 的 token,调用受保护接口。源 A 的 token 应成功,源 B 的 token 也应成功,但非法签名的 token 应被拒绝。
3. 异常确认:发送一个 issuer 不在配置列表中的 token,确认服务器是否返回 401 Unauthorized 且日志中提示未知的发行者。

使用 curl 进行快速验证:

# 测试源 A 的 token
curl -H "Authorization: Bearer <TOKEN_FROM_AUTH_A>" http://localhost:8080/api/test

# 测试源 B 的 token
curl -H "Authorization: Bearer <TOKEN_FROM_AUTH_B>" http://localhost:8080/api/test

# 测试非法 issuer 的 token (应返回 401)
curl -H "Authorization: Bearer <TOKEN_FROM_UNKNOWN_ISS>" http://localhost:8080/api/test

常见坑与风险规避

1. 安全风险:extractIssuer 方法解析了未验证签名的 token。切勿在此阶段使用 claims 做任何业务授权判断,仅用于选择 Decoder。真正的签名验证在 targetDecoder.decode(token) 中完成。
2. 性能损耗:虽然每次请求解析两次 token(一次取 iss,一次验证),但通过 ConcurrentHashMap 缓存了 Decoder 实例,避免了重复创建。NimbusJwtDecoder 内部也会缓存 JWK,确保密钥轮换正常。
3. 公钥轮换:不同认证源的 JWK 刷新策略可能不同。NimbusJwtDecoder 默认会处理 JWK 集缓存,但需确保网络可达。如果某个源密钥轮换频繁,需监控其 JWK URI 响应。
4. Claim 依赖:不要仅依赖 iss 字段,如果多个源共用同一个 iss 值但密钥不同,这种方案会失效,需结合 aud 或其他字段区分。
5. 版本差异:Spring Security 5.7 前后配置方式有变化,老项目使用 @EnableResourceServer 的需迁移到 SecurityFilterChain 模式才能灵活定制。