springboot 整合 spring session 實現 session 共享


錄:

1、分布式架構下的 session 共享問題
2、springboot 整合 spring session 的整合過程
3、簡讀 Spring Session 源碼

 

1、分布式架構下的 session 共享問題    <--返回目錄

1.1、session 的作用:

  因為 HTTP 是無狀態的協議,web 服務器為了區分記住用戶的狀態,會為每個用戶創建一個會話,存儲用戶的相關信息,以便在后面的請求中,可以定位到同一個上下文。

  例如用戶在登陸之后,在進行頁面跳轉的時候,存儲在 session 中的信息會一直保持,如果用戶還沒有 session,那么服務器會創建一個 session 對象,直到會話過期或主動放棄(退出),服務器才會把 session 終止掉。

  配合客戶端(瀏覽器)的使用,一般會使用 cookie 來管理 session。

 

1.2、分布式架構中的 session 問題

  單服務器架構下,session 直接保存在服務器中,是一點問題都沒有的。隨着分布式架構的流行,單個服務器已經不能滿足系統的需要了,通常都會把系統部署多個實例,通過負載均衡把請求分發到其中的一個實例上。這樣同一個用戶的請求可能被分發到不同的實例上,比如第一次請求訪問實例 A,創建了 session,但是下一次訪實例 B,這個時候就會出現取不到 session 的情況。於是,分布式架構中,session 共享就成了一個很大的問題。

 

1.3、分布式架構下的 session 共享問題的解決方案

  1)不要有 session:大家可能覺得我說了句廢話,但是確實在某些場景下,是可以沒有 session 的,其實在很多接口類系統當中,都提倡【API無狀態服務】;也就是每一次的接口訪問,都不依賴於 session、不依賴於前一次的接口訪問;

    - 不用 session,比如可以使用 token;

  2)存入 cookie 中:將 session 存儲到 cookie 中,但是缺點也很明顯,例如每次請求都得帶着 session,數據存儲在客戶端本地,是有風險的;

    - 即把用戶信息等數據直接存到 cookie,這樣顯然是不安全的;

  3)session 同步:對個服務器之間同步session,這樣可以保證每個服務器上都有全部的session信息,不過當服務器數量比較多的時候,同步是會有延遲甚至同步失敗;

  4)使用Nginx(或其他負載均衡軟硬件)中的ip綁定策略,同一個ip只能在指定的同一個機器訪問,但是這樣做風險也比較大,而且也是去了負載均衡的意義;

  5)我們現在的系統會把 session 放到 Redis 中存儲,雖然架構上變得復雜,並且需要多訪問一次Redis,但是這種方案帶來的好處也是很大的:實現session共享,可以水平擴展(增加Redis服務器),服務器重啟session不丟失(不過也要注意session在Redis中的刷新/失效機制),不僅可以跨服務器session共享,甚至可以跨平台(例如網頁端和APP端)。

 

  下面介紹上面解決方案 5 的一個實現:使用 Spring Session。

 

2、springboot 整合 spring session 的整合過程    <--返回目錄

參考:SpringBoot 2 整合 Spring Session 最簡操作

測試結果:

  1)訪問 http://localhost:8080/demo/add/username/zs,向 session 中添加屬性 username=zs

 

   2) 訪問 http://localhost:8081/demo/get/username, 獲取 session 中屬性 username

 

   可以看到生成的 包含 sessionId 的 cookie 的 path是 “/項目名”。所以,如果兩個項目的項目名不同,則 cookie 不能傳遞過去。這時需要自定義配置 cookie 的 path。

 

3、簡讀 Spring Session 源碼    <--返回目錄

  Spring Session 原理是:實現一個過濾器,將 原生的 request, response, session 等進行裝飾,並通過 "filterChain.doFilter(wrappedRequest, wrappedResponse);" 進行掉包,從而開發者在程序中得到的 request, response, session 都是調包后的裝飾對象。

// 過濾器 
SessionRepositoryFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);

        SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response);
        SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);

        try {
            filterChain.doFilter(wrappedRequest, wrappedResponse);
        }
        finally {
            // write the session id to the response and persist the Session
            wrappedRequest.commitSession();
        }
    }
}
// HttpSessionWrapper#commitSession()
// write the session id to the response and persist the Session
private void commitSession() {
    HttpSessionWrapper wrappedSession = getCurrentSession();
    if (wrappedSession == null) {
        if (isInvalidateClientSession()) {
            SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this, this.response);
        }
    }
    else {
        S session = wrappedSession.getSession();
        clearRequestedSessionCache();
        SessionRepositoryFilter.this.sessionRepository.save(session);
        String sessionId = session.getId();
        if (!isRequestedSessionIdValid() || !sessionId.equals(getRequestedSessionId())) {
            // 寫 cookie 到 client
            SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this, this.response, sessionId);
        }
    }
}

// 寫 cookie 到 client
public class DefaultCookieSerializer implements CookieSerializer {
    @Override
    public void writeCookieValue(CookieValue cookieValue) {
        HttpServletRequest request = cookieValue.getRequest();
        HttpServletResponse response = cookieValue.getResponse();
        StringBuilder sb = new StringBuilder();
        sb.append(this.cookieName).append('=');
        String value = getValue(cookieValue);
        if (value != null && value.length() > 0) {
            validateValue(value);
            sb.append(value);
        }
        int maxAge = getMaxAge(cookieValue);
        if (maxAge > -1) {
            sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());
            ZonedDateTime expires = (maxAge != 0) ? ZonedDateTime.now(this.clock).plusSeconds(maxAge)
                    : Instant.EPOCH.atZone(ZoneOffset.UTC);
            sb.append("; Expires=").append(expires.format(DateTimeFormatter.RFC_1123_DATE_TIME));
        }
        String domain = getDomainName(request);
        if (domain != null && domain.length() > 0) {
            validateDomain(domain);
            sb.append("; Domain=").append(domain);
        }
        String path = getCookiePath(request);
        if (path != null && path.length() > 0) {
            validatePath(path);
            sb.append("; Path=").append(path);
        }
        if (isSecureCookie(request)) {
            sb.append("; Secure");
        }
        if (this.useHttpOnlyCookie) {
            sb.append("; HttpOnly");
        }
        if (this.sameSite != null) {
            sb.append("; SameSite=").append(this.sameSite);
        }
        response.addHeader("Set-Cookie", sb.toString());
    }
}

 

  "HttpSession session = request.getSession(); " 的底層實現過程:

// 創建session
// 類型是 SessionRepositoryFilter$SessionRepositoryRequestWrapper$HttpSessionWrapper
SessionRepositoryRequestWrapper extends HttpServletRequestWrapper {
    @Override
    public HttpSessionWrapper getSession() {
        return getSession(true);
    }
    
    @Override
    public HttpSessionWrapper getSession(boolean create) {
        HttpSessionWrapper currentSession = getCurrentSession();
        if (currentSession != null) {
            return currentSession;
        }
        S requestedSession = getRequestedSession();
        // 這里代碼省略。。。
        if (!create) {
            return null;
        }
        // session 底層結構:MapSession
        S session = SessionRepositoryFilter.this.sessionRepository.createSession();
        session.setLastAccessedTime(Instant.now());
        // 使用 HttpSessionWrapper 包裝 MapSession
        currentSession = new HttpSessionWrapper(session, getServletContext());
        setCurrentSession(currentSession);
        return currentSession;
    }
        
}

// session 的底層結構:MapSession
public class RedisIndexedSessionRepository {

    @Override
    public RedisSession createSession() {
        MapSession cached = new MapSession();
        if (this.defaultMaxInactiveInterval != null) {
            cached.setMaxInactiveInterval(Duration.ofSeconds(this.defaultMaxInactiveInterval));
        }
        RedisSession session = new RedisSession(cached, true);
        session.flushImmediateIfNecessary();
        return session;
    }
}

  

  "session.setAttribute(name, value);" 的底層實現過程:

// 設置屬性
class HttpSessionAdapter<S extends Session> implements HttpSession {
    @Override
    public void setAttribute(String name, Object value) {
        checkState();
        Object oldValue = this.session.getAttribute(name);
        // 
        this.session.setAttribute(name, value);
        // 這里代碼省略。。。
    }

}

RedisIndexedSessionRepository {
    @Override
    public void setAttribute(String attributeName, Object attributeValue) {
        // 這個 cached 就是底層結構 MapSession
        this.cached.setAttribute(attributeName, attributeValue);
        this.delta.put(getSessionAttrNameKey(attributeName), attributeValue);
        // 根據配置 spring.session.redis.flush-mode=on_save/immediate 判斷
        flushImmediateIfNecessary();
    }
    
    private void flushImmediateIfNecessary() {
        if (RedisIndexedSessionRepository.this.flushMode == FlushMode.IMMEDIATE) {
            save();
        }
    }
    
    private void save() {
        saveChangeSessionId();
        saveDelta();
    }

    /**
     * Saves any attributes that have been changed and updates the expiration of this
     * session.
     */
    private void saveDelta() {
        if (this.delta.isEmpty()) {
            return;
        }
        String sessionId = getId();
        getSessionBoundHashOperations(sessionId).putAll(this.delta);
        String principalSessionKey = getSessionAttrNameKey(
                FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME);
        String securityPrincipalSessionKey = getSessionAttrNameKey(SPRING_SECURITY_CONTEXT);
        if (this.delta.containsKey(principalSessionKey) || this.delta.containsKey(securityPrincipalSessionKey)) {
            if (this.originalPrincipalName != null) {
                String originalPrincipalRedisKey = getPrincipalKey(this.originalPrincipalName);
                RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(originalPrincipalRedisKey)
                        .remove(sessionId);
            }
            Map<String, String> indexes = RedisIndexedSessionRepository.this.indexResolver.resolveIndexesFor(this);
            String principal = indexes.get(PRINCIPAL_NAME_INDEX_NAME);
            this.originalPrincipalName = principal;
            if (principal != null) {
                String principalRedisKey = getPrincipalKey(principal);
                RedisIndexedSessionRepository.this.sessionRedisOperations.boundSetOps(principalRedisKey)
                        .add(sessionId);
            }
        }

        this.delta = new HashMap<>(this.delta.size());

        Long originalExpiration = (this.originalLastAccessTime != null)
                ? this.originalLastAccessTime.plus(getMaxInactiveInterval()).toEpochMilli() : null;
        RedisIndexedSessionRepository.this.expirationPolicy.onExpirationUpdated(originalExpiration, this);
    }
}

---


免責聲明!

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



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