異常現象
當資源服務/客戶端使用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);
}
}