淺析HttpSession

蘇格拉底曰:我惟一知道的,就是本身一無所知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的概念瀏覽器

何爲HttpSession

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

如何使用HttpSession

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

HttpSession的實現機制

爲了理解清楚上述的疑問,那麼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最大可擁有量

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服務中比較經常使用的對象,理解它的含義以及應用邏輯能夠幫助咱們更好的使用它。以蘇格拉底的話來講就是我惟一知道的,就是本身一無所知

相關文章
相關標籤/搜索