使用SpringSession管理分布式会话时遇到的反序列化问题


关于SpringSession相关的介绍和使用指南,可移步如下网址:

 【SpringSession管理分布式系统的会话Session】

https://www.cnblogs.com/captainad/p/10861006.html

问题浮现

我们在使用SpringSession时(其实在问题出现时,我们并没有意识到和这儿有关联),遇到了一个隐藏较深的问题。我们像往常一样,在用户登录成功之后,将用户的实体类信息实例化保存到了session中,且session最后保存到了redis里面,这个过程其实是没有什么问题的。

随着时间的推移业务的迭代变化,用户实体需要为更新的需求增加额外的字段,于是我们顺其自然的在类中追加其他String类型的属性,结果上了测试环境,发现我们无法进入到登录页面,从浏览器控制台上我们发现这个请求因为无法获取正确的鉴权而不断报302,并且浏览器错误主页提示我们“清除浏览器cookie”,当我们照做之后,登录页面神奇般的可以打开并且我们能够顺利登录系统和使用系统了。在我们回到服务端查看后台日志时,我们看到时在redis中出现了反序列化的问题,而且日志报的很蹊跷,并不是我们的业务代码的某一行直接抛出的日志,这给我们定位问题带了一点麻烦。

分析问题

首先反序列化出现问题,我们首先能够想到的就是给用户实体加了额外属性导致的,但是为何会出现反序列化问题呢?原因看起来很简单,我们的用户实体类确实实现了Serializable接口,但是我们并没有显示的写出序列化的serialVersionUID,这在实体变更了属性字段之后反序列化回来,就一定会反序列化失败的,因为在你没有指定序列号serialVersionUID号的时候,系统默认都是以类名称类属性等来自动生成的,不同的属性名称得出的序列化值肯定是不一样的,解决的方法很简单,就是在初始化类的时候,主动写名序列化serialVersionUID号。

但是我们的程序现在已经在生产环境了,无法做到这一步,因为按照新的类来生成序列号,显然是前后不一致的,给新加的字段加上transient字段?这种做法也步行,这意味着这个字段的值将无法序列化,后面其他系统肯定是使用不了的,这些额外增加的字段就没有存在意义了。

在对报错日志进行抽丝剥茧逐行分析之后,发现这个异常居然和SpringSession有关系。

因为我们在用户成功登录系统之后,使用了request.getSession().setAttribute("xx", "xx")操作,我们将我们的用户实体放到了session中,并由SpringSession成功的放到了Redis里面,并且我们的会话时长是48小时;在我们新的实体更新进来之后,系统一发布,打开登录页面系统对SpringSession中的内容进行初始化,就发现了用户实体无法从Redis中初始化回来,因为前后的序列化号对不上了,于是就疯狂的报反序列化失败,这是根本问题所在。

解决问题

那怎么解决呢?很明显,聪明的浏览器给了我们第一个解决方案,那就是清除浏览器Session。

因为上面我们说到,SpringSession会在浏览器里面自动设置一个名为SESSION的cookie值,这个值是为了后台服务能够在Redis中找到之前存放的Session以便能够识别是统一用户,当浏览器清除了Cookie之后,这个SESSION的值就失去了,于是系统重新为这个浏览器发起请求的用户重新生成SESSION值,这个请求也就无法在redis中找到对应的Session会话,也就没有将用户实体反序列化这个过程,登录程序当然能够正常进行,而且往后的交互都是正常的了。

那么还有另外的解决方法,从Redis端,既然SpringSession会用这个SESSION去Redis中找对应的值,那么我们就将这个Redis中存放的Session内容进行清除,找不到内容自然就无需反序列化了,不过此时正常访问的用户,也会因为请求没有找到统一的Session而认为是会话过期强制退出。这里在redis中清除内容的方式,可以使用下面的命令:

$ redis-cli keys '*' | xargs redis-cli del
$ redis-cli del spring:session:sessions:7e8383a4-082c-4ffe-a4bc-c40fd3363c5e

第三种方式呢,就是从代码端改造,反序列化失败的本质就是原来的类新增了属性,而且类没有显示的指定序列化serialVersionUID,那么我们变通一下,保持原来的类不变呗,新增的属性放到一个继承了这个用户实体类的类当中,后面我们就用这个新的类,保持老的类不变(属性类型和数量等的都不变),那就行了。是的,就是使用新类的方式,新类继承用户实体类,保持用户实体类不变,新类当中增加需要补充的属性字段,同时需要序列化,这样SpringSession在初始化时,它依然能够反序列找到原来没有变通的那个类,我们自己也能够很好的实现我们新的需求了。

问题总结

通过上面的分析我们可以发现,其实SpringSession是替换了HttpSession并在序列化之后存入了Redis里面的,隐藏较深的持久化对外部没有写明serialVersionUID的对象进行的变化较为敏感,如果一旦遗漏了这个细节,在日后变更属性之后,可能就会暴露出这个问题,而且因为牵扯到用户登录鉴权,这种影响算是较为恶劣,所以平时应该留意这种小错误,尽可能在对象实现序列化之后写名serialVersionUID,相信这是一种好习惯,当然,可以设置IDEA让它自动检测,如果你实现了Serializable接口但是没有生成serialVersionUID就报错提醒。希望这次的问题分析能够对日后使用SpringSession有所帮助,能够想周到一点,想深入一点。


作者:Captain&D,发布于:2019/05/15
原文:https://www.cnblogs.com/captainad/p/10868097.html