spring security oauth2 資源服務/客戶端無法正確獲取權限


異常現象

當資源服務/客戶端使用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

  1. 跟蹤發現返回的認證信息中,集合全部被解析成了字符串
  2. 跟蹤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

  1. 跟蹤發現返回的認證信息中,集合解析為ArrayList
  2. 跟蹤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());
	...
	}
}

解決方案

以下三種都可以,按需選擇

  1. 檢查授權服務是否包含jackson-dataformat-xml依賴,刪除此依賴則默認返回json數據
  2. 自定義資源服務RemoteTokenServices,header加上Accept=“application/json”
  3. 配置授權服務器默認ContentType
@Configuration
@EnableWebMvc
public class WebConfiguration implements WebMvcConfigurer {
    @Override
    public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
        configurer.defaultContentType(MediaType.APPLICATION_JSON);
    }
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM