场景描述
这是一个微信小程序向后端发送的请求,并且请求路径被后端Spring Security权限认证监控
这里Spring Security只负责权限不负责登录认证
问题
因为微信小程序本身不支持cookie机制,但是即使手动为请求带上了登录凭证字段ticket
,请求依旧返回'用户未登录'
,接口功能不能调用
排查问题
反复尝试几次,排除请求字段等低级错误之后尝试以下做法
后端版本回滚,检查接口功能
为了做微信登陆以及适配微信小程序,开发过程中后端代码有所变更,以至于原本网页端可用的功能不可用
测试网页端相同功能请求接口,功能正常(涉及登录状态不能直接测接口)
自行设置的cookie字段是否生效
前端检查登录接口正常,也就是说:前端登录提交表单时匹配验证码携带的cookie是正常被后端接受了的,也就是cookie字段生效
结合网页端功能正常,基本确定问题出在后端
动态断点检查
通过在上面的Interceptor断点,确定了:
- 前置请求以及无须权限的请求都是进入并成功赋权了的,证实了不是
ticket
cookie字段的问题 - 出现问题的请求并没有进入Interceptor便直接返回了
'用户未登录'
,证实了问题出在Spring Security
静态代码检查
整个过程涉及的流程大概是这样:
- 首先,位于Filter层的Spring Security会对请求做权限检查
- 其次,检查登录凭证的Interceptor会检查
ticket
并为之赋权
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 从cookie中获取凭证
String ticket = CookieUtil.getValue(request,"ticket");
if (ticket!=null){
// ...检查凭证是否有效
Authentication authentication = new UsernamePasswordAuthenticationToken(
user,user.getPassword(),userService.getAuthorities(user.getId()));
SecurityContextHolder.setContext(new SecurityContextImpl(authentication));
}
}else SecurityContextHolder.clearContext();
return true;
}
- 最终到达aspect以及Controller层
猜想:流程问题
上述的整个认证验权的过程并不是简单线性的,一句话概括就是:当前请求权限取决于上一次请求,登录成功后需要一次无需权限请求刷新后续请求权限
怎么理解呢,首先登录请求肯定是无需权限也没有权限的,同时也没有登录凭证
ticket
,登录成功后,返回并给前端设置登录凭证ticket
下次带上就能通过Interceptor的认证
但是!下一次请求仍旧是没有权限的,因为上述先Filter后Interceptor的机制,登录请求本身是没携带ticket
,也就是说并没有完成Interceptor中的赋权过程
如果下一次请求是一个需要权限的请求,则会被Spring Security直接拦截
解决办法是我们会进行一次无需状态的请求,无需权限才能通过Spring Security,它携带上ticket
在Interceptor中刷新用户权限,同时这个操作也会刷新页面,登录态的页面内容跟未登录的是不一样的,所以这个一举两得的做法很巧妙
于是我在小程序端实现了上述要求,但是很让人失望的是——报错仍在,还有别的问题
报错的位置
报错是在Spring Security的错误梳理处定义的
http.exceptionHandling()
// 没有登陆时的处理
response.setContentType("application/plain;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(CommunityUtil.getJSONString(403, "用户未登录"));
})
问题定位
我百思不得其解,于是第二天早上约了做小程序的同学,详细描述并讨论了这个问题
尽管我们两个对Spring Security都不是很熟悉,但是他还是指出:Spring Security是怎么知道你这次请求和上一次请求是同一个用户呢?
换言之,我意识到我无法回答的关键问题是:Spring Security是通过请求中的什么去匹配上下文中保存的凭据,并最终判断权限是否通过的呢?
继续检查请求并简单查阅资料后,我们把目光聚焦在了JSESSIONID
字段(我并没有使用Session,而是用的Redis做分布式登录),我们猜想:Spring Secrity就是通过这个字段绑定了会话,并且正是由于这个字段的缺失导致了小程序请求被拦截
再通过多次构造请求试验后,我证实了这个猜想