开发手记-小程序请求被Spring Security权限认证拦截

场景描述

这是一个微信小程序向后端发送的请求,并且请求路径被后端Spring Security权限认证监控

这里Spring Security只负责权限不负责登录认证

问题

因为微信小程序本身不支持cookie机制,但是即使手动为请求带上了登录凭证字段ticket,请求依旧返回'用户未登录',接口功能不能调用

排查问题

反复尝试几次,排除请求字段等低级错误之后尝试以下做法

后端版本回滚,检查接口功能

为了做微信登陆以及适配微信小程序,开发过程中后端代码有所变更,以至于原本网页端可用的功能不可用

测试网页端相同功能请求接口,功能正常(涉及登录状态不能直接测接口)

自行设置的cookie字段是否生效

前端检查登录接口正常,也就是说:前端登录提交表单时匹配验证码携带的cookie是正常被后端接受了的,也就是cookie字段生效


结合网页端功能正常,基本确定问题出在后端

动态断点检查

通过在上面的Interceptor断点,确定了:

  1. 前置请求以及无须权限的请求都是进入并成功赋权了的,证实了不是ticket cookie字段的问题
  2. 出现问题的请求并没有进入Interceptor便直接返回了'用户未登录',证实了问题出在Spring Security
静态代码检查

整个过程涉及的流程大概是这样:

  1. 首先,位于Filter层的Spring Security会对请求做权限检查
  2. 其次,检查登录凭证的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;
}
  1. 最终到达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就是通过这个字段绑定了会话,并且正是由于这个字段的缺失导致了小程序请求被拦截
再通过多次构造请求试验后,我证实了这个猜想

寻找解决方案

既然确定了权限认证与JSESSIONID相关,那么最直接的解决思路便是:找到这个JSESSIONID是在何时、通过何种方式设置到前端cookie中的,再通过之前的手动设置cookie字段的方式完成这一认证过程
当然这样的解决方案谈不上好,但却可能是最简单直接的,也不会影响到原有的后端代码

手动返回并设置JSESSIONID

因为第一次以及每一次都需要这个动作,所以考虑通过最外层的Filter来实现

但是或许更进一步则是:弄清楚Spring Security通过这个JSESSIONID进行,亦或者了解Spring Security是否提供了其他更可行的方案

应急解决方案

经过了解与考虑,完善的解决方案大概有这么几种:

  1. 很明显cookie登录方案不适合微信小程序,禁用cookie的安全考虑也已经是广泛共识了,所以可以改用JWT登录方案

以前认真研究过,但是我仍然认为JWT机制没有Session方案好,在续期、强制下线等问题上考虑,但是或许对于微信小程序刚刚好

  1. 可以考虑代理服务器

但是存在3个问题:

  1. 至少暂时,我不希望大改后端代码,毕设答辩临近,我希望把精力都放在完成未实现的小程序页面和功能上
  2. 目前的后端是网页端、小程序共用的,如果改了前端的已完成代码也要大改,况且我始终不认为JWT是好的登录方案
  3. 如果要工程化地去考虑这个问题可能要花很多时间,而且可能会引入未知的难度,一卡又是好几天

所以目前的做法其实相当于是一个应急处理,详细的研究还是留到后面有时间再说

应急处理的大思路还是:手动模拟前后端的cookie交互过程
能够这么处理的原因主要基于:项目中cookie非用不可的地方其实很少,只有三处,通过精细化(细致繁琐)的操作其实是可以解决问题的

  1. 绑定验证码字段的captchaOwner:一次性
  2. JSESSIONID:几乎每次请求都要带上
  3. ticket:登录后的所有请求都需要携带,而且不能断

然后手动模拟涉及到的问题其实也有三个:

  1. 后端怎么手动提供JSESSIONID字段

我发现其实机制是这样的:如果前端请求没有JSESSIONID字段,后端就会在响应中自动添加一个SetSession来设置JSESSIONID`
这好像不是我设置的,具体是哪里工作的有待考究

  1. 前端怎么手动从Set-Cookie头中取出并设值到本地存储中

因为可能会同时存在多条Set-Cookie头,而且除了每个cookie包含了多个由;分隔的字段外,我发现字段内部会存在 Expires=Thu, 18-May-2023 17:29:03 GMT这种明明是一个字段但是中间还有,的字段,所以其实(字符串处理)并不好分
所以为了方便就为JSESSIONID设置了单独的头字段便于区区分取出,其余两个字段精细管理
captchaOwner只用一次,ticket只存一次,所以其实都还好

  1. 前端请求如何每次手动带上模拟的Cookie头字段

尝试了Taro设置拦截器统一添加,但是好像不太行,未果,同时一刀切也不方便精细操作,于是自己封了一个工具方法

目前还未优化这个应急处理,也还没仔细考虑会不会有什么次生问题,明天再继续,终于可以推进进度了

本文作者:YaosGHC

本文链接:https://www.cnblogs.com/yaocy/p/17388120.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   YaosGHC  阅读(335)  评论(0编辑  收藏  举报
历史上的今天:
2022-05-12 剑指Offer-2-实现Singleton模式
点击右上角即可分享
微信分享提示
💬
评论
📌
收藏
💗
关注
👍
推荐
🚀
回顶
收起