蘇格拉底曰:我惟一知道的,就是本身一無所知java
最近在翻閱Springboot Security板塊中的會話管理器過濾器SessionManagementFilter源碼的時候,發現其會對單用戶的多會話進行校驗控制,好比其下的某個策略ConcurrentSessionControlAuthenticationStrategy,節選部分代碼apache
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { // 獲取單用戶的多會話 final List<SessionInformation> sessions = sessionRegistry.getAllSessions( authentication.getPrincipal(), false); // 一系列判斷 int sessionCount = sessions.size(); int allowedSessions = getMaximumSessionsForThisUser(authentication); .... .... // session超出後的操做,通常是拋異常結束filter的過濾 allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry); }
筆者通常的思惟是認爲單個校驗經過的用戶有單一的會話,爲什麼會有多個會話呢?那多個會話其又是如何管理的呢?帶着疑問探究下HttpSession的概念瀏覽器
通俗的理解應該是基於HTTP協議而產生的服務器級別的對象。其獨立於客戶端發的請求,並非客戶端每一次的請求便會建立此對象,也不是客戶端關閉了就會被註銷。
故其依賴於HTTP服務器的運行,是獨立於客戶端的一種會話。目的也是保存公共的屬性供頁面間跳轉的參數傳遞。服務器
HttpSession主要是經過HttpServletRequest#getSession()方法來建立,且只依賴於此方法的建立。通常都是用戶校驗經過後,應用纔會調用此方法保存一些公共的屬性,方便頁面間傳遞。cookie
爲了理解清楚上述的疑問,那麼HttpSession的實現機制必須深刻的瞭解一下。由於其依賴於相應的HTTP服務器,就以Springboot內置的Tomcat服務器做爲分析的入口吧。session
筆者以惟一入口HttpServletRequest#getSession()方法爲源頭,倒推其代碼實現邏輯,大體梳理了下Tomcat服務器的HTTP請求步驟ide
AbstractEndpoint做爲服務的建立入口,其子類NioEndpoint則採用NIO思想建立TCP服務並運行多個Poller線程用於接收客戶端(瀏覽器)的請求--> 經過Poller#processSocket()方法調用內部類SocketProcessor來間接引用AbstractProtocol內部類ConnectionHandler處理具體的請求--> HTTP相關的請求則交由AbstractHttp11Protocol#createProcessor()方法建立Http11Processor對象處理----> Http11Processor引用CoyoteAdapter對象來包裝成org.apache.catalina.connector.Request對象來最終處理建立HttpSession--> 優先解析URL中的JSESSIONID參數,若是沒有則嘗試獲取客戶端Cookie中的JSESSIONID鍵值,最終存入至相應Session對象屬性sessionId中,避免對來自同一來源的客戶端重複建立HttpSession
基於上述的步驟用戶在獲取HttpSession對象時,會調用Request#doGetSession()方法來建立,具體的筆者不分析了。ui
總而言之,HttpSession的關鍵之處在於其對應的sessionId,每一個HttpSession都會有獨一無二的sessionId與之對應,至於sessionId的建立讀者可自行分析,只須要知道其在應用服務期間會對每一個HttpSession建立惟一的sessionId便可。this
上述講解了HttpSession的獲取方式是基於sessionId的,那麼確定有一個出口去保存相應的鍵值對,仔細一看發現其是基於cookie去實現的,附上Request#doGetSession()方法關鍵源碼線程
protected Session doGetSession(boolean create) { ..... ..... // session不爲空且支持cookie機制 if (session != null && context.getServletContext() .getEffectiveSessionTrackingModes() .contains(SessionTrackingMode.COOKIE)) { // 默認建立Key爲JSESSIONID的Cookie對象,並設置maxAge=-1 Cookie cookie = ApplicationSessionCookieConfig.createSessionCookie( context, session.getIdInternal(), isSecure()); response.addSessionCookieInternal(cookie); } if (session == null) { return null; } session.access(); return session; }
很明顯,由上述的代碼可知,HttpSession的流通還須要依賴Cookie機制的使用。此處談及一下Cookie對象中的maxAge,能夠看下其API說明
/** * Sets the maximum age of the cookie in seconds. * <p> * A positive value indicates that the cookie will expire after that many * seconds have passed. Note that the value is the <i>maximum</i> age when * the cookie will expire, not the cookie's current age. * <p> * A negative value means that the cookie is not stored persistently and * will be deleted when the Web browser exits. A zero value causes the * cookie to be deleted. * * @param expiry * an integer specifying the maximum age of the cookie in * seconds; if negative, means the cookie is not stored; if zero, * deletes the cookie * @see #getMaxAge */ public void setMaxAge(int expiry) { maxAge = expiry; }
默認maxAge值爲-1,即當瀏覽器進程重開以前,此對應的JSESSIONID的cookie值都會在訪問服務應用的時候被帶上。
由此處其實能夠理解,若是屢次重開瀏覽器進程並登陸應用,則會出現單用戶有多個session的狀況。因此纔有了限制Session最大可擁有量
這裏淺談下Springboot Security中對Session的管理,主要是針對單個用戶多session的狀況。由HttpSecurity#sessionManagement()來進行相應的配置
@Override protected void configure(HttpSecurity http) throws Exception { // 單用戶最大session數爲2 http.sessionManagement().maximumSessions(2); }
通過上述的配置,便會引入兩個關於session管理的過濾鏈,筆者按照過濾順序分開淺析
主要是針對過時的session進行相應的註銷以及退出操做,看下關鍵的處理代碼
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 獲取HttpSession HttpSession session = request.getSession(false); if (session != null) { SessionInformation info = sessionRegistry.getSessionInformation(session .getId()); if (info != null) { // 若是設置爲過時標誌,則開始清理操做 if (info.isExpired()) { // 默認使用SecurityContextLogoutHandler處理退出操做,內含session註銷 doLogout(request, response); // 事件推送,默認是直接輸出session數過多的信息 this.sessionInformationExpiredStrategy.onExpiredSessionDetected(new SessionInformationExpiredEvent(info, request, response)); return; } else { // Non-expired - update last request date/time sessionRegistry.refreshLastRequest(info.getSessionId()); } } } chain.doFilter(request, response); }
前文也說起,若是服務應用期間,要註銷session,只能調用相應的session.invalid()方法。直接看下SecurityContextLogoutHandler#logout()源碼
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { Assert.notNull(request, "HttpServletRequest required"); if (invalidateHttpSession) { HttpSession session = request.getSession(false); if (session != null) { // 註銷 session.invalidate(); } } if (clearAuthentication) { SecurityContext context = SecurityContextHolder.getContext(); context.setAuthentication(null); } // 清理上下文 SecurityContextHolder.clearContext(); }
筆者只展現ConcurrentSessionControlAuthenticationStrategy策略類用於展現session的最大值校驗
public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { // 獲取當前校驗經過的用戶所關聯的session數量 final List<SessionInformation> sessions = sessionRegistry.getAllSessions( authentication.getPrincipal(), false); int sessionCount = sessions.size(); // 最大session支持,可配置 int allowedSessions = getMaximumSessionsForThisUser(authentication); if (sessionCount < allowedSessions) { // They haven't got too many login sessions running at present return; } if (allowedSessions == -1) { // We permit unlimited logins return; } if (sessionCount == allowedSessions) { HttpSession session = request.getSession(false); if (session != null) { // Only permit it though if this request is associated with one of the // already registered sessions for (SessionInformation si : sessions) { if (si.getSessionId().equals(session.getId())) { return; } } } // If the session is null, a new one will be created by the parent class, // exceeding the allowed number } // 超出對應數的處理 allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry); }
繼續跟蹤allowableSessionsExceeded()方法
protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException { // 1.要麼拋異常 if (exceptionIfMaximumExceeded || (sessions == null)) { throw new SessionAuthenticationException(messages.getMessage( "ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[] { Integer.valueOf(allowableSessions) }, "Maximum sessions of {0} for this principal exceeded")); } // Determine least recently used session, and mark it for invalidation SessionInformation leastRecentlyUsed = null; for (SessionInformation session : sessions) { if ((leastRecentlyUsed == null) || session.getLastRequest() .before(leastRecentlyUsed.getLastRequest())) { leastRecentlyUsed = session; } } // 2.要麼設置對應的expired爲true,最後交由上述的ConcurrentSessionFilter來處理 leastRecentlyUsed.expireNow(); }
關於session的保存,你們能夠關注RegisterSessionAuthenticationStrategy註冊策略,其是排在上述的策略以後的,就是先判斷再註冊,很順暢的邏輯。筆者此處就不分析了,讀者可自行分析
HttpSession是HTTP服務中比較經常使用的對象,理解它的含義以及應用邏輯能夠幫助咱們更好的使用它。以蘇格拉底的話來講就是我惟一知道的,就是本身一無所知