問題 因為想在多個應用之間共用用戶的登錄態,因此實現了自己的 ,使用Kryo把 序列化然後放到redis之中去,同時也使用了 來使用shiro自己的存儲。然而之後一直出現丟失更新的問題,例如 分析 DEBUG之後發現,從Subject中取到的Session並不是我們在SessionDAO中創建的Si ...
問題
因為想在多個應用之間共用用戶的登錄態,因此實現了自己的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實則不是一個好註意,應該考慮其它的機制。