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實則不是一個好注意,應該考慮其它的機制。