oauth2 client访问oauth2 server 的user info 端点; 返回401 invalid_user_info_response
日志是: invalid_user_info_response] An error occurred while attempting to retrieve the UserInfo Resource: 401 null]
onAuthenticationFailure request = [org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterRequest@50eeb3fd], response = [org.springframework.security.web.header.HeaderWriterFilter$HeaderWriterResponse@5e294790], exception = [org.springframework.security.oauth2.core.OAuth2AuthenticationException: [invalid_user_info_response] An error occurred while attempting to retrieve the UserInfo Resource: 401 null]
通过抓包,发现,Cannot convert access token to JSON
具体是
请求:
GET http://192.168.1.103:8081/auth/user/me HTTP/1.1
Accept: application/json
Authorization: Bearer 036685d2-ed5a-48c7-a5c7-a135a9e49468
User-Agent: Java/1.8.0_231
Host: 192.168.1.103:8081
Connection: keep-alive
响应:
HTTP/1.1 401
Access-Control-Allow-Origin: *
Access-Control-Allow-Methods: *
Access-Control-Max-Age: 3600
Access-Control-Allow-Headers: x-requested-with, authorization
Pragma: no-cache
WWW-Authenticate: Bearer realm="oauth2-resource", error="invalid_token", error_description="Cannot convert access token to JSON"
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Date: Wed, 13 Jul 2022 08:02:17 GMT
Content-Length: 83
{"error":"invalid_token","error_description":"Cannot convert access token to JSON"}
测试发现就是,如果把 自己注册的TokenStore 去掉就好了,为什么呢?
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
调试半天,终于发现:
@Configuration
public class ResourceServerConfiguration extends WebSecurityConfigurerAdapter implements Ordered {
private int order = 3;
@Autowired(required = false)
private TokenStore tokenStore;
@Autowired(required = false)
private AuthenticationEventPublisher eventPublisher;
@Autowired(required = false)
private Map<String, ResourceServerTokenServices> tokenServices;
@Autowired
private ApplicationContext context;
private List<ResourceServerConfigurer> configurers = Collections.emptyList();
@Autowired(required = false)
private AuthorizationServerEndpointsConfiguration endpoints;
...
}
原来是 ResourceServerConfiguration里面有@Autowired,把我创建的JwtTokenStore注册进去了,但是呢
调试发现其实有多个 DefaultTokenServices实例, 有的是使用InMemoryTokenStore(测试发现是 AuthorizationServer), ResourceServer使用的是JwtTokenStore
这样就出问题了,AuthorizationServer 返回的是普通的access_token:
然后oauth2 client 使用它访问oauth2 server 的user info 端点, 然后ResourceServer使用JwtTokenStore 来解析它,尝试从其中获取 用户信息,结果就自然解析不了,报错了!
注意:
AuthorizationServerEndpointsConfigurer、 ResourceServerSecurityConfigurer 各有各自的 tokenStore,两者最好是相同。
解决方法是,在配置自定义的AuthorizationServerConfigurerAdapter的时候,给endpoints的tokenStore也设置为相同jwtTokenStore, 这样就不会有转换问题了
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.accessTokenConverter(jwtAccessTokenConverter)
.tokenStore(jwtTokenStore)
;
super.configure(endpoints);
}
...
更正
测试发现,仅仅配置.tokenStore(jwtTokenStore) 是不行的,其实只需要配置 accessTokenConverter 为jwtAccessTokenConverter即可;也就是:
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
.accessTokenConverter(jwtAccessTokenConverter)
;
super.configure(endpoints);
}
...
why,因为accessTokenConverter 才是真正复杂发放 accessToken 的地方, 而且注意AuthorizationServerEndpointsConfigurer的tokenStore()
tokenStore() 其实会尝试根据accessTokenConverter生成tokenStore, 如果JwtAccessTokenConverter则生成JwtTokenStore
private TokenStore tokenStore() {
if (tokenStore == null) {
if (accessTokenConverter() instanceof JwtAccessTokenConverter) {
this.tokenStore = new JwtTokenStore((JwtAccessTokenConverter) accessTokenConverter());
}
else {
this.tokenStore = new InMemoryTokenStore();
}
}
return this.tokenStore;
}
然后就可以看到oauth/token端点返回正确的jwt格式的token了!
请求:
POST http://192.168.1.103:8081/auth/oauth/token HTTP/1.1
Accept: application/json;charset=UTF-8
Content-Type: application/x-www-form-urlencoded;charset=UTF-8
Authorization: Basic UjJkcHhRM3ZQcnRmZ0Y3MjpmRHc3TXBrazVjekhOdVNSdG1oR21BR0w0MkNheFFCOQ==
User-Agent: Java/1.8.0_231
Host: 192.168.1.103:8081
Connection: keep-alive
Content-Length: 98
grant_type=authorization_code&code=3Xfg5J&redirect_uri=http%3A%2F%2F192.168.1.103%3A8082%2FdoLogin
响应:
{"access_token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTc3NDI5OTcsInVzZXJfbmFtZSI6ImEiLCJhdXRob3JpdGllcyI6WyJST0xFX1VTRVIiXSwianRpIjoiODNjNzNkYWEtMWM2OC00NTBmLWI0MDEtZWU3YjRhZWQ5ZDMyIiwiY2xpZW50X2lkIjoiUjJkcHhRM3ZQcnRmZ0Y3MiIsInNjb3BlIjpbInVzZXJfaW5mbyJdfQ.mq1tHm4DMOgIgIvQd1JcoKhqDPR79cxOLV9g3sKnv8U","token_type":"bearer","expires_in":43152,"scope":"user_info","jti":"83c73daa-1c68-450f-b401-ee7b4aed9d32"}