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

Spring Boot分离Auth与Resource Server的JWT/OAuth2配置问题

关于Spring OAuth2 JWT资源服务器的问题与解决方案

问题背景

我正在构建一个基于JWT令牌和OAuth2协议的Spring应用,认证服务器(Authentication Server)已经按照教程正常运行,但资源服务器(Resource Server)始终无法正常工作。现在请求资源服务器时收到invalid_token错误,同时代码编译还抛出了密钥相关的异常,有两个核心问题需要解决:

  • 使用JwtTokenStore时,如何处理分离部署的认证服务器与资源服务器?
  • 按照JWT和OAuth规范,资源服务器应该可以委托认证服务器验证Token,尝试配置security.oauth2.resource.token-info-uri=http://localhost:8080/oauth/check_token但配置失败,移除本地密钥后仍报相同的invalid_token错误。

相关配置代码

资源服务器Security配置类

@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Value("${security.signing-key}")
    private String signingKey;
    @Value("${security.encoding-strength}")
    private Integer clientID; // 注意:这里变量名写错了!应该是encodingStrength
    @Value("${security.security-realm}")
    private String securityRealm;

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(signingKey);
        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    ResourceServerTokenServices tokenService() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }

    @Override
    public AuthenticationManager authenticationManager() throws Exception {
        OAuth2AuthenticationManager authManager = new OAuth2AuthenticationManager();
        authManager.setTokenServices(tokenService());
        return authManager;
    }
}

资源服务器配置类

@Configuration @EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    private ResourceServerTokenServices tokenServices;
    @Value("${security.jwt.resource-ids}")
    private String resourceIds;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(resourceIds).tokenServices(tokenServices);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.requestMatchers().and().authorizeRequests().antMatchers("/actuator/**", "/api-docs/**").permitAll()
                .antMatchers("/**").authenticated();
    }
}

认证服务器Security配置类(参考教程)

@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Value("${security.signing-key}")
    private String signingKey;
    @Value("${security.encoding-strength}")
    private Integer encodingStrength;
    @Value("${security.security-realm}")
    private String securityRealm;

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    @Override
    protected AuthenticationManager authenticationManager() throws Exception {
        return super.authenticationManager();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(new ShaPasswordEncoder(encodingStrength));
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .httpBasic()
                .realmName(securityRealm)
                .and()
                .csrf()
                .disable();
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(signingKey);
        return converter;
    }

    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean @Primary //避免同名TokenService实例冲突
    public DefaultTokenServices tokenServices() {
        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore());
        defaultTokenServices.setSupportRefreshToken(true);
        return defaultTokenServices;
    }
}

错误信息

  1. 请求资源服务器时返回:

{"error":"invalid_token","error_description":"Invalid access token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdGp3dHJlc291cmNlaWQiXSwidXNlcl9uYW1lIjoiam9obi5kb2UiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXh wIjoxNTE1MTE3NTU4LCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF"}

  1. 编译时抛出异常:

Caused by: java.lang.IllegalStateException: For MAC signing you do not need to specify the verifier key separately, and if you do it must match the signing key


解决方案

先解决编译错误:密钥配置问题

你遇到的IllegalStateException是因为MAC签名(比如HS256这类对称加密算法)不需要单独指定验证密钥,如果指定了必须和签名密钥完全一致。你的认证服务器用的是converter.setSigningKey(signingKey)(对称加密),但资源服务器却调用了converter.setVerifierKey(signingKey),这就触发了这个错误。

修复方法:资源服务器的JwtAccessTokenConverter配置改成和认证服务器一致,用setSigningKey代替setVerifierKey:

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
    JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
    converter.setSigningKey(signingKey); // 这里改成setSigningKey
    return converter;
}

另外注意资源服务器Security配置里的变量名错误:private Integer clientID;应该改成private Integer encodingStrength;,避免配置注入错误。


问题1:使用JwtTokenStore处理分离的Auth与Resource Server

当使用JwtTokenStore时,JWT本身是自包含的(所有用户信息、权限都在Token里),所以资源服务器不需要和认证服务器共享数据库或Token存储,只需要持有相同的签名密钥就能验证Token的合法性。

正确的配置要点:

  • 认证服务器和资源服务器必须使用相同的签名密钥(security.signing-key配置项一致)
  • 资源服务器的JwtAccessTokenConverter必须和认证服务器的配置完全匹配(比如加密算法、密钥)
  • 确保资源服务器配置的resourceIds和认证服务器中客户端配置的resourceIds一致

问题2:委托认证服务器验证Token(使用check_token端点)

如果你不想让资源服务器持有密钥,想委托认证服务器验证Token,需要使用RemoteTokenServices代替DefaultTokenServices,而不是自己配置JwtTokenStore。

具体配置步骤:

  1. 移除资源服务器中所有和JwtAccessTokenConverter、JwtTokenStore相关的Bean
  2. 配置RemoteTokenServices作为ResourceServerTokenServices:
@Bean
public ResourceServerTokenServices tokenServices() {
    RemoteTokenServices tokenServices = new RemoteTokenServices();
    tokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
    tokenServices.setClientId("你的客户端ID"); // 要和认证服务器里配置的客户端ID一致
    tokenServices.setClientSecret("你的客户端密钥"); // 要和认证服务器里配置的客户端密钥一致
    return tokenServices;
}
  1. 确保认证服务器的check_token端点是可访问的:认证服务器的Security配置要允许这个端点的请求(默认是允许的,但如果有自定义拦截需要放开)
  2. 移除资源服务器配置文件中security.signing-key相关的配置,不需要本地密钥了

另外,要注意:使用RemoteTokenServices时,资源服务器每次验证Token都要调用认证服务器的check_token接口,会有网络开销;而使用JwtTokenStore是本地验证,性能更好,适合高并发场景,你可以根据自己的需求选择。


内容的提问来源于stack exchange,提问作者KellyM

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.15 07:14:44