Shiro權限管理框架(四):深刻分析Shiro中的Session管理

其實關於Shiro的一些學習筆記很早就該寫了,由於懶癌和拖延症晚期一直沒有落實,直到今天公司的一個項目碰到了在集羣環境的單點登陸頻繁掉線的問題,爲了解決這個問題,Shiro相關的文檔和教程沒少翻。最後問題解決了,但我以爲我也是時候來作一波Shiro學習筆記了。html

本篇是Shiro系列第四篇,Shiro中的過濾器初始化流程和實現原理。Shiro基於URL的權限控制是經過Filter實現的,本篇從咱們注入的ShiroFilterFactoryBean開始入手,翻看源碼追尋Shiro中的過濾器的實現原理。java

首發地址:https://www.guitu18.com/post/2019/08/08/45.html緩存

Session服務器

SessionManagersession

咱們在配置Shiro時配置了一個DefaultWebSecurityManager,先來看下分佈式

DefaultWebSecurityManageride

public DefaultWebSecurityManager() {
        super();
        ((DefaultSubjectDAO) this.subjectDAO).setSessionStorageEvaluator(new DefaultWebSessionStorageEvaluator());
        this.sessionMode = HTTP_SESSION_MODE;
        setSubjectFactory(new DefaultWebSubjectFactory());
        setRememberMeManager(new CookieRememberMeManager());
        setSessionManager(new ServletContainerSessionManager());
    }

在它的構造方法中注入了一個ServletContainerSessionManageroop

public class ServletContainerSessionManager implements WebSessionManager {
    public Session getSession(SessionKey key) throws SessionException {
        if (!WebUtils.isHttp(key)) {
            String msg = "SessionKey must be an HTTP compatible implementation.";
            throw new IllegalArgumentException(msg);
        }
        HttpServletRequest request = WebUtils.getHttpRequest(key);
        Session session = null;
        HttpSession httpSession = request.getSession(false);
        if (httpSession != null) {
            session = createSession(httpSession, request.getRemoteHost());
        }
        return session;
    }
    
    private String getHost(SessionContext context) {
        String host = context.getHost();
        if (host == null) {
            ServletRequest request = WebUtils.getRequest(context);
            if (request != null) {
                host = request.getRemoteHost();
            }
        }
        return host;
    }

    protected Session createSession(SessionContext sessionContext) throws AuthorizationException {
        if (!WebUtils.isHttp(sessionContext)) {
            String msg = "SessionContext must be an HTTP compatible implementation.";
            throw new IllegalArgumentException(msg);
        }
        HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);
        HttpSession httpSession = request.getSession();
        String host = getHost(sessionContext);
        return createSession(httpSession, host);
    }
    
    protected Session createSession(HttpSession httpSession, String host) {
        return new HttpServletSession(httpSession, host);
    }
}

ServletContainerSessionManager自己並無論理會話,它最終操做的仍是HttpSession,因此只能在Servlet容器中起做用,它不能支持除使用HTTP協議的以外的任何會話。post

因此通常咱們配置Shiro都會配置一個DefaultWebSessionManager,它繼承了DefaultSessionManager,看看DefaultSessionManager的構造方法:學習

public DefaultSessionManager() {
    this.deleteInvalidSessions = true;
    this.sessionFactory = new SimpleSessionFactory();
    this.sessionDAO = new MemorySessionDAO();
}

這裏的sessionDAO初始化了一個MemorySessionDAO,它其實就是一個Map,在內存中經過鍵值對管理Session。

public MemorySessionDAO() {
    this.sessions = new ConcurrentHashMap<Serializable, Session>();
}

HttpServletSession

public class HttpServletSession implements Session {
    public HttpServletSession(HttpSession httpSession, String host) {
        if (httpSession == null) {
            String msg = "HttpSession constructor argument cannot be null.";
            throw new IllegalArgumentException(msg);
        }
        if (httpSession instanceof ShiroHttpSession) {
            String msg = "HttpSession constructor argument cannot be an instance of ShiroHttpSession.  This " +
                    "is enforced to prevent circular dependencies and infinite loops.";
            throw new IllegalArgumentException(msg);
        }
        this.httpSession = httpSession;
        if (StringUtils.hasText(host)) {
            setHost(host);
        }
    }
    protected void setHost(String host) {
        setAttribute(HOST_SESSION_KEY, host);
    }
    public void setAttribute(Object key, Object value) throws InvalidSessionException {
        try {
            httpSession.setAttribute(assertString(key), value);
        } catch (Exception e) {
            throw new InvalidSessionException(e);
        }
    }
}

Shiro的HttpServletSession只是對javax.servlet.http.HttpSession進行了簡單的封裝,因此在Web應用中對Session的相關操做最終都是對javax.servlet.http.HttpSession進行的,好比上面代碼中的setHost()是將內容以鍵值對的形式保存在httpSession中。

先來看下這張圖:

先了解上面這幾個類的關係和做用,而後咱們想管理Shiro中的一些數據就很是方便了。

SessionDao是Session管理的頂層接口,定義了Session的增刪改查相關方法。

public interface SessionDAO {
    Serializable create(Session session);
    Session readSession(Serializable sessionId) throws UnknownSessionException;
    void update(Session session) throws UnknownSessionException;
    void delete(Session session);
    Collection<Session> getActiveSessions();
}

AbstractSessionDao是一個抽象類,在它的構造方法中定義了JavaUuidSessionIdGenerator做爲SessionIdGenerator用於生成SessionId。它雖然實現了create()和readSession()兩個方法,但具體的流程調用的是它的兩個抽象方法doCreate()和doReadSession(),須要它的子類去幹活。

public abstract class AbstractSessionDAO implements SessionDAO {
    private SessionIdGenerator sessionIdGenerator;
    public AbstractSessionDAO() {
        this.sessionIdGenerator = new JavaUuidSessionIdGenerator();
    }
    
    public Serializable create(Session session) {
        Serializable sessionId = doCreate(session);
        verifySessionId(sessionId);
        return sessionId;
    }
    protected abstract Serializable doCreate(Session session);

    public Session readSession(Serializable sessionId) throws UnknownSessionException {
        Session s = doReadSession(sessionId);
        if (s == null) {
            throw new UnknownSessionException("There is no session with id [" + sessionId + "]");
        }
        return s;
    }
    protected abstract Session doReadSession(Serializable sessionId);
}

看上面那張類圖AbstractSessionDao的子類有三個,查看源碼發現CachingSessionDAO是一個抽象類,它並無實現這兩個方法。在它的子類EnterpriseCacheSessionDAO中實現了doCreate()和doReadSession(),但doReadSession()是一個空實現直接返回null。

public EnterpriseCacheSessionDAO() {
        setCacheManager(new AbstractCacheManager() {
            @Override
            protected Cache<Serializable, Session> createCache(String name) throws CacheException {
                return new MapCache<Serializable, Session>(name, new ConcurrentHashMap<Serializable, Session>());
            }
        });
    }

EnterpriseCacheSessionDAO依賴於它的父級CachingSessionDAO,在他的構造方法中向父類注入了一個AbstractCacheManager的匿名實現,它是一個基於內存的SessionDao,它所建立的MapCache就是一個Map。

咱們在Shiro配置類裏經過 sessionManager.setSessionDAO(new EnterpriseCacheSessionDAO()); 來使用它。而後在CachingSessionDAO.getCachedSession() 打個斷點測試一下,能夠看到cache就是一個ConcurrentHashMap,在內存中以Key-Value的形式保存着JSESSIONID和Session的映射關係。

再來看AbstractSessionDao的第三個實現MemorySessionDAO,它就是一個基於內存的SessionDao,簡單直接,構造方法直接new了一個ConcurrentHashMap。

public MemorySessionDAO() {
        this.sessions = new ConcurrentHashMap<Serializable, Session>();
    }

那麼它和EnterpriseCacheSessionDAO有啥區別,其實EnterpriseCacheSessionDAO只是CachingSessionDAO的一個默認實現,在CachingSessionDAO中cacheManager是沒有默認值的,在EnterpriseCacheSessionDAO的構造方法將其初始化爲一個ConcurrentHashMap。

若是咱們直接用EnterpriseCacheSessionDAO其實和MemorySessionDAO其實沒有什麼區別,都是基於Map的內存型SessionDao。可是CachingSessionDAO的目的是爲了方便擴展的,用戶能夠繼承CachingSessionDAO並注入本身的Cache實現,好比以Redis緩存Session的RedisCache。

其實際業務上若是須要Redis來管理Session,那麼直接繼承AbstractSessionDao更好,有Redis支撐中間的Cache就是多餘的,這樣還能夠作分佈式或集羣環境的Session共享(分佈式或集羣環境若是中間還有一層Cache那麼還要考慮同步問題)。

回到開篇提到的咱們公司項目集羣環境下單點登陸頻繁掉線的問題,其實就是中間那層Cache形成的。咱們的業務代碼中RedisSessionDao是繼承自EnterpriseCacheSessionDAO的,這樣一來那在Redis之上還有一個基於內存的Cache層。此時用戶的Session若是發生變動,雖然Redis中的Session是同步的,Cache層沒有同步,致使的現象就是用戶在一臺服務器的Session是有效的,另外一臺服務器Cache中的Session仍是舊的,而後用戶就被迫下線了。

相關文章
相關標籤/搜索