开发手记-小程序请求被Spring Security权限认证拦截
场景描述
这是一个微信小程序向后端发送的请求,并且请求路径被后端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就是通过这个字段绑定了会话,并且正是由于这个字段的缺失导致了小程序请求被拦截
再通过多次构造请求试验后,我证实了这个猜想
寻找解决方案
既然确定了权限认证与JSESSIONID
相关,那么最直接的解决思路便是:找到这个JSESSIONID
是在何时、通过何种方式设置到前端cookie中的,再通过之前的手动设置cookie字段的方式完成这一认证过程
当然这样的解决方案谈不上好,但却可能是最简单直接的,也不会影响到原有的后端代码
手动返回并设置JSESSIONID
因为第一次以及每一次都需要这个动作,所以考虑通过最外层的Filter来实现
但是或许更进一步则是:弄清楚Spring Security通过这个JSESSIONID
进行,亦或者了解Spring Security是否提供了其他更可行的方案
应急解决方案
经过了解与考虑,完善的解决方案大概有这么几种:
- 很明显cookie登录方案不适合微信小程序,禁用cookie的安全考虑也已经是广泛共识了,所以可以改用JWT登录方案
以前认真研究过,但是我仍然认为JWT机制没有Session方案好,在续期、强制下线等问题上考虑,但是或许对于微信小程序刚刚好
- 可以考虑代理服务器
但是存在3个问题:
- 至少暂时,我不希望大改后端代码,毕设答辩临近,我希望把精力都放在完成未实现的小程序页面和功能上
- 目前的后端是网页端、小程序共用的,如果改了前端的已完成代码也要大改,况且我始终不认为JWT是好的登录方案
- 如果要工程化地去考虑这个问题可能要花很多时间,而且可能会引入未知的难度,一卡又是好几天
所以目前的做法其实相当于是一个应急处理,详细的研究还是留到后面有时间再说
应急处理的大思路还是:手动模拟前后端的cookie交互过程
能够这么处理的原因主要基于:项目中cookie非用不可的地方其实很少,只有三处,通过精细化(细致繁琐)的操作其实是可以解决问题的
- 绑定验证码字段的captchaOwner:一次性
- JSESSIONID:几乎每次请求都要带上
- ticket:登录后的所有请求都需要携带,而且不能断
然后手动模拟涉及到的问题其实也有三个:
- 后端怎么手动提供
JSESSIONID
字段
我发现其实机制是这样的:如果前端请求没有JSESSIONID
字段,后端就会在响应中自动添加一个
SetSession来设置
JSESSIONID`
这好像不是我设置的,具体是哪里工作的有待考究
- 前端怎么手动从
Set-Cookie
头中取出并设值到本地存储中
因为可能会同时存在多条
Set-Cookie
头,而且除了每个cookie包含了多个由;
分隔的字段外,我发现字段内部会存在Expires=Thu, 18-May-2023 17:29:03 GMT
这种明明是一个字段但是中间还有,
的字段,所以其实(字符串处理)并不好分
所以为了方便就为JSESSIONID
设置了单独的头字段便于区区分取出,其余两个字段精细管理
captchaOwner
只用一次,ticket
只存一次,所以其实都还好
- 前端请求如何每次手动带上模拟的
Cookie
头字段
尝试了Taro设置拦截器统一添加,但是好像不太行,未果,同时一刀切也不方便精细操作,于是自己封了一个工具方法
目前还未优化这个应急处理,也还没仔细考虑会不会有什么次生问题,明天再继续,终于可以推进进度了
本文作者:YaosGHC
本文链接:https://www.cnblogs.com/yaocy/p/17388120.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
2022-05-12 剑指Offer-2-实现Singleton模式