Spring Boot分离Auth与Resource Server的JWT/OAuth2配置问题
问题背景
我正在构建一个基于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; } }
错误信息
- 请求资源服务器时返回:
{"error":"invalid_token","error_description":"Invalid access token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsidGVzdGp3dHJlc291cmNlaWQiXSwidXNlcl9uYW1lIjoiam9obi5kb2UiLCJzY29wZSI6WyJyZWFkIiwid3JpdGUiXSwiZXh wIjoxNTE1MTE3NTU4LCJhdXRob3JpdGllcyI6WyJTVEFOREFSRF"}
- 编译时抛出异常:
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。
具体配置步骤:
- 移除资源服务器中所有和
JwtAccessTokenConverter、JwtTokenStore相关的Bean - 配置
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; }
- 确保认证服务器的
check_token端点是可访问的:认证服务器的Security配置要允许这个端点的请求(默认是允许的,但如果有自定义拦截需要放开) - 移除资源服务器配置文件中
security.signing-key相关的配置,不需要本地密钥了
另外,要注意:使用RemoteTokenServices时,资源服务器每次验证Token都要调用认证服务器的check_token接口,会有网络开销;而使用JwtTokenStore是本地验证,性能更好,适合高并发场景,你可以根据自己的需求选择。
内容的提问来源于stack exchange,提问作者KellyM

