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); } }
---