背景
OAuth2是当下比较流行的鉴权模型,其基础概念这里不在表述,有兴趣的同学可以看下这篇文章: 图解OAuth2
本文主要讲解OAuth2服务鉴权相关过程源码,以及鉴权失败异常(401 nobody)解决方案
举例:
例如我有一个资源服务器接口: http://localhost:7778/resource_server
1 直接请求此接口,一定会抛出错误,因为我没有带请求token,没有访问权限
2 请求携带token,鉴权通过 http://localhost:7778/resource_server?access_token=5d8d92b6-e065-48c0-bdcb-888e004490a4 访问成功
整体流程
1 资源服务器收到访问请求,该请求被oauth2拦截器拦截
2 资源服务器获取请求中的token信息 并且调用认证服务器接口http://localhost:7777/oauth/check_token?token=ff8e79cf-e6d8-49f0-bc47-2bb813741372 进行token认证,校验token是否有效,过期等,如图
3 token校验成功,访问目标地址
细致源码追踪
1 资源服务器收到访问请求,该请求被oauth2拦截器拦截
2 准备进行token 校验
在这里 我们看到 校验token的方法,oauth2 本身提供了四个实现类, 默认选用了
RemoteTokenServices.loadAuthentication(String accessToken)方法校验
复制代码
当然这里为什么会默认选用这个类呢? 我们可以启动debug 启动的时候看到,如图
3 通过spring的restrestTemplate进行方法调用获取调用结果
4 校验过程报错 401 nobody (我在测试的时候 这边restTemplate 一直这个报错,导致校验失败,接口请求失败)
其中一段报错信息:
2022-01-14 17:27:47.733 ERROR 11989 --- [nio-7778-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception
org.springframework.web.client.HttpClientErrorException$Unauthorized: 401 : [no body]
at org.springframework.web.client.HttpClientErrorException.create(HttpClientErrorException.java:105) ~[spring-web-5.2.8.RELEASE.jar:5.2.8.RELEASE]
复制代码
401 nody 解决方案
1 因为我本地postman 跑了一遍测试token链接, 确认我这个token解析是没有问题的,并且也手写HttpUtil测试了,大量debug之后 如图:
在下大胆断定,应是底层调用restTemplate 自身问题,但是底层默认是选用
RemoteTokenServices.loadAuthentication(String accessToken) restTemplate方法校验
复制代码
因此必须要改变调 这个默认的校验方式:
2 修改校验方式,这里我选择了UserInfoTokenServices的方式,配置方法见代码
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 设置 auth2 远程 认证服务器 校验方法类
resources.tokenServices(new UserInfoTokenServices("http://localhost:7777/oauth/check_token"
, "cms"));
super.configure(resources);
}
复制代码
当然这边选择UserInfoTokenServices的方式之后,Debug发现 这个方法的请求地址有些问题 如图,会在拼装最终校验地址时,缺少token参数,见我debug图
3 终极解决方案 因上述步骤2 采用了UserInfoTokenServices的方式,但是在底层拼装请求参数时,缺少token参数, 因此我在UserInfoTokenServices 这个类的基础上,重新写了一个校验方法类UserInfoTokenServicesMySelf 主要是在入口处,将token参数进行了封装,如图
@Override
public OAuth2Authentication loadAuthentication(String accessToken)
throws AuthenticationException, InvalidTokenException {
// userInfoEndpointUrl 进行参数包装
Map<String, Object> map = getMap(this.userInfoEndpointUrl + "?token=" + accessToken, 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);
}
复制代码
并且将校验方法类UserInfoTokenServicesMySelf 配置成 ResourceServerTokenServices的实现类, 即将 原来的校验方法类由 RemoteTokenServices替换成手动扩展的 UserInfoTokenServicesMySelf 如图:
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 设置 auth2 远程 认证服务器 校验方法类
resources.tokenServices(new UserInfoTokenServicesMySelf("http://localhost:7777/oauth/check_token"
, "cms"));
super.configure(resources);
}
复制代码
最终效果展示
1 token校验通过, 目标url访问成功
2 token自身过期失效,提示报错
3 未携带token,提示错误