前言
開心一刻
老公酷愛網絡遊戲,老婆無奈,只得告誡他:你玩就玩了,但是千萬不可以在遊戲裏找老婆,不然,哼哼。。。 老公嘴角露出了微笑:放心吧親愛的,我絕對不會在遊戲裏找老婆的!因爲我有老公! 老婆:......
路漫漫其修遠兮,吾將上下而求索!
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驗證 } }
第一次創建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(); // 什麼也沒做,供繼承,便於拓展 } }
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; }
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); } }
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; // 拋出異常供上層統計用 } }
通過捕獲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); } }
若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供上層使用 } }
校驗總結
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》