Shiro使用Redis作存储之后更新Session失败的问题
问题
因为想在多个应用之间共享用户的登录态,因此实现了自己的sessiondao
,使用kryo把simplesession
序列化然后放到redis之中去,同时也使用了shiro.usernativesessionmanager: true
来使用shiro自己的存储。然而之后一直出现丢失更新的问题,例如
session session = securityutils.getsubject().getsession(); user user = (user) session.getattribute(membershipconst.sessionkey.user); user.setname("newname"); // 名称没有更新
分析
debug之后发现,从subject中取到的session并不是我们在sessiondao中创建的simplesession,而是delegatingsubject$stoppingawareproxiedsession
,这是一个代理类,本身并不做任何事情,而是通过delegatingsession
调用真正的方法。而delegatingsession实则也并没有真正的调用simplesession,而是调用的sessionmanager中的方法:
/** * @see session#setattribute(object key, object value) */ public void setattribute(object attributekey, object value) throws invalidsessionexception { if (value == null) { removeattribute(attributekey); } else { sessionmanager.setattribute(this.key, attributekey, value); } }
而默认的defaultsessionmanager
在进行任何写操作之前总是会先通过sessiondao读一次,如setattribute方法
public void setattribute(sessionkey sessionkey, object attributekey, object value) throws invalidsessionexception { if (value == null) { removeattribute(sessionkey, attributekey); } else { session s = lookuprequiredsession(sessionkey); s.setattribute(attributekey, value); onchange(s); } }
这就是了,实际上我们并未显式的将session写回redis,而是更新lastaccesstime的时候一并写回去的,而更新访问时间的时候调用了touch()
方法,sessionmanager又通过sessiondao读取了一次,重新读取了redis然后反序列化出一个新的session,原来session的各种改动自然也就丢失了。
解决
首先是在sessiondao上加上缓存,一来避免频繁的redis读取,二来避免出现每次读取返回一个新session的问题。然后在我们的场景中并不需要最后访问时间,因此重写了shirofilterfactorybean
,不在更新最后访问时间,当session需要更新的时候,直接调用sessiondao写回redis,避免sessionmanager做二传手。
当然这不是完美的解决方案,并发场景下依然会有更新问题。调式中可以看出shiro通过sessiondao进行的读写操作非常频繁,显然在设计时并未将它当作一个涉及外部io的类。因此将session放在redis实则不是一个好注意,应该考虑其它的机制。