1. 程式人生 > >shiro原始碼篇 - shiro的session的查詢、重新整理、過期與刪除,你值得擁有

shiro原始碼篇 - shiro的session的查詢、重新整理、過期與刪除,你值得擁有

前言

  開心一刻  

    老公酷愛網路遊戲,老婆無奈,只得告誡他:你玩就玩了,但是千萬不可以在遊戲裡找老婆,不然,哼哼。。。
    老公嘴角露出了微笑:放心吧親愛的,我絕對不會在遊戲裡找老婆的!因為我有老公!
    老婆:......

  路漫漫其修遠兮,吾將上下而求索!

  github:https://github.com/youzhibing

  碼雲(gitee):https://gitee.com/youzhibing

前情回顧

  大家還記得上篇博文講了什麼嗎,我們來一起簡單回顧下:

    SecurityManager是shiro的核心,負責與shiro的其他元件進行互動;SessionManager是session的真正管理者,負責shiro的session管理;

    SessionsSecurityManager的start方法中將session的建立委託給了具體的sessionManager,是建立session的關鍵入口。

    SimpleSession是shiro完完全全的自己實現,是shiro對session的一種拓展;實現了ValidatingSession介面,具有自我校驗的功能;一般不對外暴露,暴露的往往是他的代理:DelegatingSession;SimpleSession有幾個屬性值得重點關注下,如下

        id:就是session id;

        startTimestamp:session的建立時間;

        stopTimestamp:session的失效時間;

        lastAccessTime:session的最近一次訪問時間,初始值是startTimestamp

        timeout:session的有效時長,預設30分鐘

        expired:session是否到期

        attributes:session的屬性容器

查詢

  session的建立完成後,會將session(SimpleSession型別)物件的代理物件(DelegatingSession)裝飾成StoppingAwareProxiedSession物件,然後繫結到subject(型別是DelegatingSubject);

  Session session = subject.getSession();返回的就是繫結在當前subjuct的session。注意subject的實際型別是:DelegatingSubject,如下圖

重新整理

  shiro的Session介面提供了一個touch方法,負責session的重新整理;session的代理物件最終會呼叫SimpleSession的touch():

public void touch() {
    this.lastAccessTime = new Date();        // 更新最後被訪問時間為當前時間
}

  但是touch方法是什麼時候被呼叫的呢?JavaSE需要我們自己定期的呼叫session的touch() 去更新最後訪問時間;如果是Web應用,每次進入ShiroFilter都會自動呼叫session.touch()來更新最後訪問時間,ShiroFilter的類圖如下:

  ShiroFilter自動呼叫session.touch()如下

過期

  如果是讓我們自己實現session過期的判斷,我們會怎麼做了?我們來看看shiro是怎麼做的,或許我們能夠從中學到一些經驗。

  啟動校驗定時任務

    還記得AbstractValidatingSessionManager中createSession方法嗎?在呼叫doCreateSession方法之前呼叫enableSessionValidationIfNecessary(),enableSessionValidationIfNecessary程式碼如下

private void enableSessionValidationIfNecessary() {
    // 獲取session驗證排程器
    SessionValidationScheduler scheduler = getSessionValidationScheduler();
    // session驗證排程器開啟 && (排程器為空或排程器不可用)
    if (isSessionValidationSchedulerEnabled() && (scheduler == null || !scheduler.isEnabled())) {
        enableSessionValidation();        // 開啟session驗證
    }
}
View Code

    第一次建立session的時候,如果session驗證排程器啟用(預設是啟用),那麼呼叫enableSessionValidation(),enableSessionValidation程式碼如下

protected synchronized void enableSessionValidation() {
    SessionValidationScheduler scheduler = getSessionValidationScheduler();        // 獲取調取器
    if (scheduler == null) {
        scheduler = createSessionValidationScheduler();                            // 建立調取器,實際型別是ExecutorServiceSessionValidationScheduler
        setSessionValidationScheduler(scheduler);                                // 將排程器繫結到sessionManager
    }
    // it is possible that that a scheduler was already created and set via 'setSessionValidationScheduler()'
    // but would not have been enabled/started yet
    if (!scheduler.isEnabled()) {
        if (log.isInfoEnabled()) {
            log.info("Enabling session validation scheduler...");
        }
        scheduler.enableSessionValidation();                                    // 啟動定時任務,驗證session
        afterSessionValidationEnabled();                                        // 什麼也沒做,供繼承,便於拓展
    }
}
View Code

    ExecutorServiceSessionValidationScheduler類圖如下,它實現了Runnable介面

  呼叫scheduler的enableSessionValidation(),enableSessionValidation方法如下

public void enableSessionValidation() {
    if (this.interval > 0l) {
        // 建立ScheduledExecutorService
        this.service = Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {  
            private final AtomicInteger count = new AtomicInteger(1);

            public Thread newThread(Runnable r) {  
                Thread thread = new Thread(r);  
                thread.setDaemon(true);  
                thread.setName(threadNamePrefix + count.getAndIncrement());
                return thread;  
            }  
        });
        //  初始化service interval時長之後開始執行this的run方法,每隔interval執行一次;注意interval的單位是TimeUnit.MILLISECONDS
        this.service.scheduleAtFixedRate(this, interval, interval, TimeUnit.MILLISECONDS);    // this就是ExecutorServiceSessionValidationScheduler自己
    }
    this.enabled = true;
}
View Code

  session校驗 

  定時(預設每隔60分鐘)的呼叫ExecutorServiceSessionValidationScheduler的run方法,run方法中呼叫sessionManager的validateSessions方法來完成session的驗證,validateSessions方法如下

/**
 * @see ValidatingSessionManager#validateSessions()
 */
public void validateSessions() {
    if (log.isInfoEnabled()) {
        log.info("Validating all active sessions...");
    }

    int invalidCount = 0;

    // 從sessionDao中獲取全部的session
    // sessionDao可以是預設的MemorySessionDAO,也可以是我們定製的CachingSessionDAO
    Collection<Session> activeSessions = getActiveSessions();

    if (activeSessions != null && !activeSessions.isEmpty()) {
        // 一個一個校驗
        for (Session s : activeSessions) {
            try {
                //simulate a lookup key to satisfy the method signature.
                //this could probably stand to be cleaned up in future versions:
                SessionKey key = new DefaultSessionKey(s.getId());
                validate(s, key);        // 真正校驗的方法
            } catch (InvalidSessionException e) {
                if (log.isDebugEnabled()) {
                    boolean expired = (e instanceof ExpiredSessionException);
                    String msg = "Invalidated session with id [" + s.getId() + "]" +
                            (expired ? " (expired)" : " (stopped)");
                    log.debug(msg);
                }
                invalidCount++;            // 統計上次到這次定時任務間隔內過期的session個數
            }
        }
    }

    if (log.isInfoEnabled()) {
        String msg = "Finished session validation.";
        if (invalidCount > 0) {
            msg += "  [" + invalidCount + "] sessions were stopped.";
        } else {
            msg += "  No sessions were stopped.";
        }
        log.info(msg);
    }
}
View Code

  validate方法如下

protected void validate(Session session, SessionKey key) throws InvalidSessionException {
    try {
        doValidate(session);                    // 真正校驗session
    } catch (ExpiredSessionException ese) {
        onExpiration(session, ese, key);        // 從sessionDao中刪除過期的session
        throw ese;                                // 丟擲異常供上層統計用
    } catch (InvalidSessionException ise) {
        onInvalidation(session, ise, key);        // 從sessionDao中刪除不合法的session
        throw ise;                                // 丟擲異常供上層統計用
    }
}
View Code

  通過捕獲doValidate()丟擲的異常來剔除過期的或不合法的session,並將異常接著往上拋,供上層統計過期數量。注意:ExpiredSessionException的父類是StoppedSessionException,而StoppedSessionException的父類是InvalidSessionException。

  doValidate方法如下

protected void doValidate(Session session) throws InvalidSessionException {
    if (session instanceof ValidatingSession) {
        ((ValidatingSession) session).validate();        // 校驗session是否過期
    } else {                                            // 若session不是ValidatingSession型別,則丟擲IllegalStateException異常
        String msg = "The " + getClass().getName() + " implementation only supports validating " +
                "Session implementations of the " + ValidatingSession.class.getName() + " interface.  " +
                "Please either implement this interface in your session implementation or override the " +
                AbstractValidatingSessionManager.class.getName() + ".doValidate(Session) method to perform validation.";
        throw new IllegalStateException(msg);
    }
}
View Code

    若session不是ValidatingSession型別,則丟擲IllegalStateException異常

  validate方法如下

public void validate() throws InvalidSessionException {
    //check for stopped:
    if (isStopped()) {                    
    // sesson已經停止了,則丟擲StoppedSessionException;理論上來講不會出現這種情況,但程式的事沒有100%保障
        //timestamp is set, so the session is considered stopped:
        String msg = "Session with id [" + getId() + "] has been " +
                "explicitly stopped.  No further interaction under this session is " +
                "allowed.";
        throw new StoppedSessionException(msg);
    }

    //check for expiration
    if (isTimedOut()) {     // 校驗是否過期,校驗方法是:lastAccessTime是否小於(當前時間 - session有效時長)
        expire();            // 更新session的stopTimestamp為當前時間,session的expired為true

        //throw an exception explaining details of why it expired:
        Date lastAccessTime = getLastAccessTime();
        long timeout = getTimeout();

        Serializable sessionId = getId();

        DateFormat df = DateFormat.getInstance();
        String msg = "Session with id [" + sessionId + "] has expired. " +
                "Last access time: " + df.format(lastAccessTime) +
                ".  Current time: " + df.format(new Date()) +
                ".  Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds (" +
                timeout / MILLIS_PER_MINUTE + " minutes)";
        if (log.isTraceEnabled()) {
            log.trace(msg);
        }
        throw new ExpiredSessionException(msg);    // 丟擲ExpiredSessionException供上層使用
    }
}
View Code

  校驗總結

    1、sesion的有效時長預設30分鐘;定時任務預設是每60分鐘執行一次,第一次執行是在定時器初始化完成60分鐘後執行;

    2、session不是ValidatingSession型別,則丟擲IllegalStateException異常;session已經停止了則丟擲StoppedSessionException;session過期則丟擲ExpiredSessionException異常;理論上來講IllegalStateException與StoppedSessionException不會被丟擲,應該全是ExpiredSessionException異常;ExpiredSessionException繼承自StoppedSessionException,而StoppedSessionException又繼承自IllegalStateException;

    3、校驗session的時候,丟擲了異常,將其捕獲,從sessionDao中刪除對應的session,並使過期數量自增1

刪除

  夾雜在過期定時任務中,與過期是同時進行的,利用的異常機制;當然session操作的時候sessionManager也有session的校驗,伴隨著就有session的刪除。

疑問

  定時任務預設每60分鐘執行一次,而session有效時長預設是30分鐘,那麼定時任務執行的間隔內肯定有session過期了,而我們在這個間隔內操作了過期的session怎麼辦

  其實這個問題應該這麼來問:在定時任務間隔期間,對session的操作有沒有做校驗處理?答案是肯定的。

  通過上面的講解我們知道:session的操作通過代理之後,都會來到sessionManager,sessionManager通過處理之後再到SimpleSession;AbstractNativeSessionManager中將session操作放給SimpleSession之前,都會呼叫lookupSession方法,跟進lookupSession你會發現,裡面也有session的校驗。

  所以session的校驗,不只是定製任務在執行,很多session的操作都有做session的校驗。

總結

  1、一般我們操作subject是DelegatingSubject型別,DelegatingSubject中將subject的操作委託給了securityManager;一般操作的session是session的代理,代理將session操作委託給sessionManager,sesionManager校驗之後再轉交給SimpleSession;

  2、session過期定時任務預設60分鐘執行一次,所session已過期或不合法,則丟擲對應的異常,上層通過捕獲異常從sessionDao中刪除session

  3、不只定時任務做session的校驗,session的基本操作都在sessionManager中有做session的校驗

參考

  《跟我學shiro》