1. 程式人生 > >Shiro使用Redis作儲存之後更新Session失敗的問題

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