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);
    }
}

posted on 2020-08-05 16:48  路过君  阅读(2551)  评论(0编辑  收藏  举报

导航