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
- 跟踪发现返回的认证信息中,集合全部被解析成了字符串
- 跟踪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);
}
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?