shiro的session的查詢、重新整理、過期與刪除,你值得擁有
前言
開心一刻
老公酷愛網路遊戲,老婆無奈,只得告誡他:你玩就玩了,但是千萬不可以在遊戲裡找老婆,不然,哼哼。。。 老公嘴角露出了微笑:放心吧親愛的,我絕對不會在遊戲裡找老婆的!因為我有老公! 老婆:......
路漫漫其修遠兮,吾將上下而求索!
前情回顧
大家還記得上篇博文講了什麼嗎,我們來一起簡單回顧下:
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》