淺析HttpSession


蘇格拉底曰:我唯一知道的,就是自己一無所知

源頭

最近在翻閱Springboot Security板塊中的會話管理器過濾器SessionManagementFilter源碼的時候,發現其會對單用戶的多會話進行校驗控制,比如其下的某個策略ConcurrentSessionControlAuthenticationStrategy,節選部分代碼

	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的概念

何為HttpSession

通俗的理解應該是基於HTTP協議而產生的服務器級別的對象。其獨立於客戶端發的請求,並不是客戶端每一次的請求便會創建此對象,也不是客戶端關閉了就會被注銷。
故其依賴於HTTP服務器的運行,是獨立於客戶端的一種會話。目的也是保存公共的屬性供頁面間跳轉的參數傳遞。

如何使用HttpSession

HttpSession主要是通過HttpServletRequest#getSession()方法來創建,且只依賴於此方法的創建。一般都是用戶校驗通過后,應用才會調用此方法保存一些公共的屬性,方便頁面間傳遞。

HttpSession的實現機制

為了理解清楚上述的疑問,那么HttpSession的實現機制必須深入的了解一下。因為其依賴於相應的HTTP服務器,就以Springboot內置的Tomcat服務器作為分析的入口吧。

代碼層

筆者以唯一入口HttpServletRequest#getSession()方法為源頭,倒推其代碼實現邏輯,大致梳理了下Tomcat服務器的HTTP請求步驟

	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()方法來創建,具體的筆者不分析了。

總而言之,HttpSession的關鍵之處在於其對應的sessionId,每個HttpSession都會有獨一無二的sessionId與之對應,至於sessionId的創建讀者可自行分析,只需要知道其在應用服務期間會對每個HttpSession創建唯一的sessionId即可。

保存方式

上述講解了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最大可擁有量

HttpSession的管理

這里淺談下Springboot Security中對Session的管理,主要是針對單個用戶多session的情況。由HttpSecurity#sessionManagement()來進行相應的配置

    @Override
    protected void configure(HttpSecurity http) throws Exception {
	    // 單用戶最大session數為2
        http.sessionManagement().maximumSessions(2);
    }

經過上述的配置,便會引入兩個關於session管理的過濾鏈,筆者按照過濾順序分開淺析

ConcurrentSessionFilter

主要是針對過期的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();
	}

SessionManagementFilter

筆者只展示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服務中比較常用的對象,理解它的含義以及應用邏輯可以幫助我們更好的使用它。以蘇格拉底的話來說就是我唯一知道的,就是自己一無所知


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM