异常现象
当资源服务/客户端使用token-info-uri校验token时无法获取全部的授权权限,只能获取其中一个权限,使用user-info-uri则可以获取全部的授权权限
spring security 版本2.3.8
资源服务配置
security:
oauth2:
client:
client-id: client1
client-secret: client1pwd
access-token-uri: 'http://localhost:11000/oauth/token'
user-authorization-uri: 'http://localhost:11000/oauth/authorize'
scope: all
resource:
token-info-uri: 'http://localhost:11000/oauth/check_token'
user-info-uri: 'http://localhost:11000/oauth/check_user'
prefer-token-info: true
- prefer-token-info默认值为true,既优先使用token-info-uri校验token认证信息
- prefer-token-info设置为false,或不配置token-info-uri则会使用user-info-uri,适用于需要获取userdetails信息的场景
源码跟踪
1. 授权服务
- org.springframework.security.oauth2.provider.endpoint.CheckTokenEndpoint
public class CheckTokenEndpoint {
@RequestMapping(value = "/oauth/check_token", method = RequestMethod.POST)
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, Object> response = (Map<String, Object>)accessTokenConverter.convertAccessToken(token, authentication);
// gh-1070
response.put("active", true); // Always true if token exists and not expired
return response;
}
}
跟踪发现返回的信息中authorities字段是一个集合
2. 资源服务
使用token-info-uri
- 跟踪发现返回的认证信息中,集合全部被解析成了字符串
- 跟踪org.springframework.web.client.HttpMessageConverterExtractor
发现返回的响应信息为xml,其中authorities集合被序列化为多个<authorities>元素,而没有被正确反序列化为集合类型
- org.springframework.security.oauth2.provider.token.RemoteTokenServices
public class RemoteTokenServices implements ResourceServerTokenServices {
// 校验令牌获取认证信息
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
// 发送post请求调用token-info-uri,获取认证信息
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
if (logger.isDebugEnabled()) {
logger.debug("check_token returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
// gh-838
if (map.containsKey("active") && !"true".equals(String.valueOf(map.get("active")))) {
logger.debug("check_token returned active attribute: " + map.get("active"));
throw new InvalidTokenException(accessToken);
}
return tokenConverter.extractAuthentication(map);
}
// 发送post请求
private Map<String, Object> postForMap(String path, MultiValueMap<String, String> formData, HttpHeaders headers) {
if (headers.getContentType() == null) {
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
}
@SuppressWarnings("rawtypes")
Map map = restTemplate.exchange(path, HttpMethod.POST,
new HttpEntity<MultiValueMap<String, String>>(formData, headers), Map.class).getBody();
@SuppressWarnings("unchecked")
Map<String, Object> result = map;
// 返回令牌信息
return result;
}
}
使用user-info-url
- 跟踪发现返回的认证信息中,集合解析为ArrayList
- 跟踪org.springframework.web.client.HttpMessageConverterExtractor发现返回的响应信息为json
- org.springframework.boot.autoconfigure.security.oauth2.resourceUserInfoTokenServices
public class UserInfoTokenServices implements ResourceServerTokenServices {
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
Map<String, Object> map = getMap(this.userInfoEndpointUrl, accessToken);
if (map.containsKey("error")) {
if (this.logger.isDebugEnabled()) {
this.logger.debug("userinfo returned error: " + map.get("error"));
}
throw new InvalidTokenException(accessToken);
}
return extractAuthentication(map);
}
}
真相在这里
进一步跟踪发现:
请求user-info-url时header.Accept=“application/json”
请求token-info-url时header.Accept=“application/xml, text/xml, application/json, application/+xml, application/+json”,如果授权服务器支持xml格式contenttype则会有限返回xml格式
- org.springframework.boot.autoconfigure.security.oauth2.resource.DefaultUserInfoRestTemplateFactory
public class DefaultUserInfoRestTemplateFactory implements UserInfoRestTemplateFactory {
@Override
public OAuth2RestTemplate getUserInfoRestTemplate() {
...
// 此处加入了拦截器,为请求头加上Accept="application/json"
this.oauth2RestTemplate.getInterceptors()
.add(new AcceptJsonRequestInterceptor());
...
}
}
解决方案
以下三种都可以,按需选择
- 检查授权服务是否包含jackson-dataformat-xml依赖,删除此依赖则默认返回json数据
- 自定义资源服务RemoteTokenServices,header加上Accept=“application/json”
- 配置授权服务器默认ContentType
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
@Override
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
configurer.defaultContentType(MediaType.APPLICATION_JSON);
}
}