開始進行 Web 開發時,您可能在使用 Session 時會碰到 Cookie 和 LocalStorage,被它們所幹擾。由於他們均可以存儲數據,有過時時間,不須要在使用時從新請求。您還會遇到這樣的狀況,Web 容器(例如 Tomcat、Jetty)包含 Session 的實現,當服務器重啓以後,以前的登陸狀態會失效須要從新登陸。前端
HTTP 協議spring
咱們先從 HTTP 協議提及。HTTP 協議有個特色,是無狀態的,意味着請求與請求是沒有關係的。早期的 HTTP 協議只是用來簡單地瀏覽網頁,沒有其餘需求,所以請求與請求之間不須要關聯。但現代的 Web 應用功能很是豐富,能夠網上購物、支付、遊戲、聽音樂等等。若是請求與請求之間沒有關聯,就會出現一個很尷尬的問題:Web 應用不知道您是誰。例如,用戶登陸以後在購物車中添加了三件商品到購物車,刷新一下網頁,用戶仍然處於未登陸的狀態,購物車裏空空如也。很顯然這種狀況是不可接受的。爲此 HTTP 協議急需一種技術讓請求與請求之間創建起聯繫來標識用戶。因而出現了 Cookie 技術。數據庫
Cookie 技術express
Cookie 是 HTTP 報文的一個請求頭,Web 應用能夠將用戶的標識信息或者其餘一些信息(用戶名等等)存儲在 Cookie 中。用戶通過驗證以後,每次 HTTP 請求報文中都包含 Cookie;固然服務端爲了標識用戶,即便不通過登陸驗證,也能夠存放一個惟一的字符串用來標識用戶。採用 Cookie 就解決了用戶標識的問題,同時 Cookie 中包含有用戶的其餘信息。Cookie 本質上就是一份存儲在用戶本地的文件,裏面包含了須要在每次請求中傳遞的信息。但 Cookie 存在如下缺點:後端
Cookie 具備時效性,超過過時時間就會失效。瀏覽器
服務提供商利用 cookie 惡意蒐集用戶信息,例如用戶在未登陸的狀況下去商城瀏覽了商品,商城就會把廣告公司的 Cookie 加入到用戶的瀏覽器中,每當用戶瀏覽和廣告公司合做的網站時,都會看到以前在商城瀏覽過的相似商品。緩存
每次 Cookie 都會把除用戶標識以外的其餘用戶信息也在 Cookie 中傳遞,增長了請求的流量開銷。安全
Session 技術服務器
Cookie 以明文的方式存儲了用戶信息,形成了很是大的安全隱患,而 Session 的出現解決這個問題。用戶信息能夠以 Session 的形式存儲在後端。這樣當用戶請求到來時,請求能夠和 Session 對應起來,當後端處理請求時,能夠從 Session 中獲取用戶信息。那麼 Session 是怎麼和請求對應起來的?答案是經過 Cookie,在 Cookie 中填充一個相似 SessionID 之類的字段用來標識請求。這樣用戶的信息存在後端,相對安全,也不須要在 Cookie 中存儲大量信息浪費流量。但前端想要獲取用戶信息,例如暱稱,頭像等信息,依然須要請求後端接口去獲取這些信息。cookie
Session 管理
經過 Cookie、Session 這些技術,服務端能夠標識到不一樣的用戶,從而提供一些個性化服務。隨着用戶規模的增加,一個應用有多個實例,部署在不一樣的 Web 容器中。所以應用不可能再依賴單一的 Web 容器來管理 Session,須要將 Session 管理拆分出來。爲了實現 Session 管理,須要實現如下兩點:
Session 管理須要接入高可用,高性能的存儲組件。
有一種可靠的失效機制,當 Session 過時時,將 Session 失效掉。
爲此常見的 Session 管理都會採用高性能的存儲方式來存儲 Session,例如 Redis 和 MemCache,而且經過集羣的部署,防止單點故障,提高高可用性。而後採用定時器,或者後臺輪詢的方式在 Session 過時時將 Session 失效掉。
Spring Session 應運而生,它是一種流行的 Session 管理實現方式,相比上文提到的,Spring Session 作的要更多。Spring Session 並不和特定的協議如 HTTP 綁定,實現了一種廣義上的 Session,支持 WebSocket 和 WebSession 以及多種存儲類型如 Redis、MongoDB 等等。
Spring Session 由核心模塊和具體存儲方式相關聯的實現模塊構成。核心模塊包含了 Spring Session 的基本抽象和 API。Spring Session 有兩個核心組件:Session 和 SessionRepository。Spring Session 簡單易用,經過 SessionRepository 來操做 Session。當創建會話時,建立 Session,將一些用戶信息(例如用戶 ID)存到 Session 中,並經過 SessionRepository 將 Session 持久化。當會話從新創建的時候,能夠獲取到 Session 中的信息。同時後臺維護了一個定時任務,經過一些巧妙的方式,將過時的 Session 經過 SessionRepository 刪除掉。下面詳細介紹一下這兩個核心組件。
Session
Session 即會話,這裏的 Session 指的是廣義的 Session 並不和特定的協議如 HTTP 綁定,支持 HttpSession、WebSocket Session,以及其餘與 Web 無關的 Session。Session 能夠存儲與用戶相關的信息或者其餘信息,經過維護一個鍵值對(Key-Value)來存儲這些信息。Session 接口簽名如清單 1 所示:
清單 1. Session 接口
public interface Session { String getId(); <T> T getAttribute(String attributeName); Set<String> getAttributeNames(); void setAttribute(String attributeName, Object attributeValue); void removeAttribute(String attributeName); }
如下是相關參數介紹:
getId:每一個 Session 都有一個惟一的字符串用來標識 Session。
getAttribute:獲取 Session 中的數據,須要傳遞一個 name 獲取對應的存儲數據,返回類型是泛型,不須要進行強制轉換。
getAttributeNames:獲取 Session 中存儲信息全部的 name(也就是 Key)。
setAttribute:填充或修改 Session 中存儲的數據。
removeAttribute:刪除 Session 中填充的數據。
Session 因其存儲方式的不一樣,支持如下多種實現方式:
GemFireSession:採用 GemFire 做爲數據源,在金融領域應用很是普遍。
HazelcastSession:採用 Hazelcast 做爲數據源。
JdbcSession:採用關係型數據庫做爲數據源,支持 SQL。
MapSession:採用 Java 中的 Map 做爲數據源,通常做爲快速啓動的 demo 使用。
MongoExpiringSession:採用 MongoDB 做爲數據源。
RedisSession:採用 Redis 做爲數據源。
以上存儲方式中,採用 Redis 做爲數據源很是流行,所以下文將重點討論 Spring Session 在 Redis 中實現。
SessionRepository
SessionRepository 用來增刪改查 Session 在對應數據源中的接口。SessionRepository 的接口簽名如清單 2 所示:
清單 2. SessionRepository 接口
public interface SessionRepository<S extends Session> { S createSession(); void save(S session); S getSession(String id); void delete(String id); }
如下是相關參數介紹:
createSession:建立 Session。
Session:更新 Session。
getSession:根據 ID 來獲取 Session。
delete:根據 ID 來刪除 Session。
在 Spring Session 中最經常使用的數據源爲 Redis,本部分將重點介紹 Spring Session 如何在 Redis 中實現。Spring Session 建立 Session 後,使用 SessionRepository 將 Session 持久化到 Redis 中。當 Session 中的數據更新時,Redis 中的數據也會更新;當 Session 被從新訪問刷新時,Redis 中的過時時間也會刷新;當 Redis 中的數據失效時,Session 也會失效。
採用 Redis 做爲存儲對應的實現類
前文提到的 Session 和 SessionRepository 組件,Spring Session 採用 Redis 做爲存儲方式時,都有對應的實現方式,即下面兩個實現類。
RedisSession
Session 在採用 Redis 做爲存儲方式時,對應的實現類爲 RedisSession。RedisSession 並不直接實現 Session, 而是實現了 ExpiringSession。ExpiringSession 增長了一些屬性,用來判斷 Session 是否失效,ExpiringSession 繼承 Session。RedisSession 的接口簽名如清單 3 所示:
清單 3. RedisSession 接口
final class RedisSession implements ExpiringSession { private final MapSession cached; private Long originalLastAccessTime; String, Object> delta = new HashMap<String, Object>(); private boolean isNew; private String originalPrincipalName; }
如下是相關參數介紹:
cached:採用 MapSession 做爲緩存,意味着查找 Session 中的信息先從 MapSession 中查找,而後再從 Redis 中查找。
originalLastAccessTime:上一次訪問時間。
delta:與 Session 中的更新數據相關。
isNew:RedisSession 是不是新建的、未被更新過。
originalPrincipalName:主題名稱。
Session 在 Redis 中以 HashMap 的結構方式存儲。
RedisOperationsSessionRepository
SessionRepository 在採用 Redis 做爲存儲方式時,對應的實現類爲 RedisOperationSessionRepository。RedisOperationSessionRepository 並不直接實現 SessionRepository,而是實現了 FindByIndexNameSessionRepository。FindByIndexNameSessionRepository 繼承 SessionRepository,並提供了強大的 Session 查找接口。FindByIndexNameSessionRepository 接口如清單 4 所示:
清單 4.RedisOperationsSessionRepository 接口
public class RedisOperationsSessionRepository implements FindByIndexNameSessionRepository <RedisOperationsSessionRepository.RedisSession>, MessageListener { static final String DEFAULT_SPRING _SESSION_REDIS_PREFIX = "spring:session:"; static final String CREATION_TIME_ATTR = "creationTime"; static final String MAX_INACTIVE_ATTR = "maxInactiveInterval"; static final String LAST_ACCESSED_ATTR = "lastAccessedTime"; static final String SESSION_ATTR_PREFIX = "sessionAttr:"; }
如下是相關參數介紹:
DEFAULT_SPRING_SESSION_REDIS_PREFIX:Spring Session 在 Redis 中存儲 Session 的前綴。
CREATION_TIME_ATTR:Session 的建立時間。
MAX_INACTIVE_ATTR:Session 的有效時間。
LAST_ACCESSED_ATTR:Session 的上次使用時間。
SESSION_ATTR_PREFIX:例如在 Session 中存儲了 name 屬性,value 爲小明,Session 在 Redis 中以 HashMap 的方式,那麼 name 的存儲方式爲 sessionAttr:name, value 爲小明。
Session 在 Redis 中的存儲結構
SessionRepository 存儲 Session,本質上是在操做 Redis,如清單 5 所示:
清單 5. Session 在 Redis 中的存儲
1. HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:name lee sessionAttr:mobile 18381111111 2. EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100 3. APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe "" 4. EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800 5. SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 6. EXPIRE spring:session:expirations1439245080000 2100
在 Redis 中全部 Key 的前綴都是 spring:session(與上文中的 DEFAULT_SPRING_SESSION_REDIS_PREFIX)相對應。假設多個項目共用一個 Redis,這時須要改變前綴,前綴中能夠加入項目名如 lily 變爲 lily:spring:session。
在 Redis 中建立 Session
建立 Session 時會填充一個惟一的字符串用來標識 Session。在 Redis 中會爲 Session 設置如下屬性 creationTime、maxInactiveInterval 和 lastAccessedTime 與上文中的建立時間、有效時間、上次訪問時間相對應。Session 中填充了兩個屬性 name 和 mobile。Session 的建立如清單 6 所示:
清單 6. Session 建立
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:name lee sessionAttr:mobile 18381111111 EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
Session 在 Redis 中建立以後觸發 SessionCreatedEvent,建立 Session 後須要額外的邏輯能夠訂閱該事件。注意,Session 中的失效時間屬性 maxInactiveInterval 的值爲 1800,但在 Redis 中 Session 的失效時間爲 2100,這涉及到 Session 在 Redis 中的失效機制,下文會詳細解答。
在 Redis 中實現 Session 失效
Redis 提供了失效機制,能夠爲鍵值對設置失效期。試想一下,用 Redis 實現一個最簡單的 Session 失效,能夠爲存儲在 Redis 中的 Session 直接設置失效,時間設置爲 1800 便可。但 Spring Session 爲何沒有這樣作呢?這是 Spring Session 爲應用提供的一個擴展點,當 Session 失效時,Spring Session 能夠經過消息訂閱的方式通知到應用,應用可能會作出一些本身的邏輯處理。所以 Spring Session 新增長了 Expiration Key,爲 Expiration Key 設置失效時間爲 1800,如清單 7 所示:
清單 7. Expiration Key
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe "" EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
當 Expiration Key 被刪除以後會觸發 SessionDestroyEvent (內含 Session 相關信息)。Spring Session 會清除 Expiration Redis 中的 Session。可是存在這樣一個問題,Redis 沒法保證當 Key 過時沒法訪問時可以觸發 SessionDestroyEvent。Redis 後臺維護了一個任務,去定時地檢測 Key 是否失效(不可訪問),若是失效會觸發 SessionDestroyEvent。可是這個任務的優先級很是低,頗有可能 Key 已經失效了,但檢測任務沒有分配到執行時間片去觸發 SessionDestroyEvent。更多關於 Redis 中 Key 失效的細節參考 Timing of expired events。
爲了解決這個問題,Spring Session 根據整點分鐘數維護了一個集合,根據 Expiration Key 的失效時間將其填充到 expirations:整點分鐘數的集合中,如清單 8 所示:
清單 8. expirations 集合
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe EXPIRE spring:session:expirations1439245080000 2100
Spring Session 後臺會維護一個定時任務去檢測符合整點分鐘數的 expirations 集合,而後訪問其中的 Expiration Key。若是 Expiration Key 已經失效,Redis 會自動刪除 Expiration Key 並觸發 SessionDestroyEvent,這樣 Spring Session 會清理掉已經觸發 SessionDestroyEvent 的 Session。Spring Session 維護的定時任務代碼在 RedisOperationsSessionRepository 中,如清單 9 所示:
清單 9. Spring Session 定時任務
@Scheduled(cron = "${spring.session.cleanup.cron.expression:0 * * * * *}") public void cleanupExpiredSessions() { this.expirationPolicy.cleanExpiredSessions(); }
定時任務每分鐘的 0 秒開始執行,若是開發人員以爲這個頻率過高,能夠經過自定義 spring.session.cleanup.corn.expression 進行更改任務的執行時間。
經過上述分析,咱們發現 Spring Session 設計的很是巧妙。Spring Session 並不會根據 expirations 集合中的內容去刪除 Expiration Key。而是對可能失效的 Expiration Key 進行請求,讓 Redis 自身判斷 Key 是否已經失效,若是失效則進行清除,觸發刪除事件。此外,在 Redis 集羣中,若是不採用分佈式鎖(會極大的下降性能),Redis 可能會錯誤的把一個 Key 標記爲失效,若是冒然的刪除 Key 會致使出錯。採用請求 Expiration Key 的方式,Redis 自身會作出正確的判斷。
Spring Session 是與協議無關的,所以想要在 Web 中使用 Spring Session 須要進行集成。一個很常見的問題是:Spring Session 在 Web 中的入口是哪裏?答案是 Filter。Spring Session 選擇 Filter 而不是 Servlet 的方式有如下優勢:Spring Session 依賴 J2EE 標準,無需依賴特定的 MVC 框架。另外一方面 Spring MVC 經過 Servlet 作請求轉發,若是 Spring Session 採用 Servlet,那麼 Spring Session 和 Spring MVC 的集成會存在問題。
Spring Session 與 Web 集成的時候,須要用到如下 4 個核心組件:SessionRepositoryFilter、SessionRepositoryRequestWrapper、SessionRepositoryResponseWrapper 和 MultiHttpSessionStrategy,它們的協做方式以下:
當請求到來的時候,SessionRepositoryFilter 會攔截請求,採用包裝器模式,將 HttpServletRequest 進行包裝爲 SessionRepositoryRequestWrapper。
SessionRepositoryRequestWrapper 會覆蓋 HttpServletRequest 本來的 getSession()方法。getSession() 會改變 Session 的獲取和存儲方式,開發人員能夠本身定義採用某種方式,例如 Redis、數據庫等來獲取 Session。用戶獲取到 Session 以後,可能會對 Session 作出改變,開發人員不須要手動的對 Session 進行提交和持久化,SpringSession 將自動完成。
SessionRepositoryFilter 將 HttpServletResponse 包裝爲 SessionRepositoryResponseWrapper,並覆蓋 SessionRepositoryResponseWrapper 生命週期函數 onResponseCommitted(當請求處理完畢,該函數會被調用)。
在 onResponseCommitted 函數中,會調用 HttpSessionStrategy 確保 Session 被正確地持久化。這樣 Session 在 HTTP 的整個生命週期就完成了。
下面經過解析各組件的源碼來講明 Spring Session 如何與 Web 集成。
SessionRepositoryFilter
SessionRepositoryFilter 攔截全部請求,對 HttpServletRequest 進行包裝處理生成 SessionRepositoryRequestWrapper,對 HttpServletResponse 進行包裝處理生成 SessionRepositoryResponseWrapper。SessionRepositoryFilter 的核心代碼,如清單 10 所示:
清單 10. doFilterInternal 方法
doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain){ SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response); HttpServletRequest strategyRequest = this.httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse); HttpServletResponse strategyResponse = this.httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse); }
注意 SessionRepositoryFilter 必須放置在任何訪問或者進行 commit 操做以前,由於只有這樣才能保證 J2EE 的 Session 被 Spring Session 提供的 Session 進行復寫並進行正確的持久化。
SessionRepositoryRequestWrapper
SessionRepositoryRequestWrapper 是 HttpServletRequest 包裝類,並覆蓋 getSession 方法。getSession 方法會作以下操做:
調用 MultiHttpSessionStrategy 生成和獲取 Session 的惟一標識符 ID。
調用 SessionRepository 生成和獲取 Session。
getSession(boolean create)的代碼如清單 11 所示:
清單 11. getSession 方法
@Override public HttpSessionWrapper getSession(boolean create) { ...... String requestedSessionId = getRequestedSessionId(); if (requestedSessionId != null && getAttribute(INVALID_SESSION_ID_ATTR) == null) { S session = getSession(requestedSessionId); ...... } ...... S session = SessionRepositoryFilter .this.sessionRepository.createSession(); }
getRequestedSessionId 方法用來獲取 Session 的 ID,本質上就是調用 MultiHttpSessionStrategy 來獲取,如清單 12 所示:
清單 12. getRequestedSessionId 方法
@Override public String getRequestedSessionId() { return SessionRepositoryFilter.this.httpSessionStrategy .getRequestedSessionId(this); }
getSession(String id)方法用來獲取 Session,本質上是調用 SessionRepository 來查找 Session,如清單 13 所示:
清單 13. getSession 方法
private S getSession(String sessionId) { S session = SessionRepositoryFilter .this.sessionRepository.getSession(sessionId); if (session == null) { return null; } session.setLastAccessedTime (System.currentTimeMillis()); return session; }
SessionRepositoryResponseWrapper
SessionRepositoryResponseWrapper 是 HttpServletResponse 的包裝類,覆蓋了 onResponseCommitted 方法。主要職責是檢測 Session 是否失效,若是失效進行相應處理;確保新建立的 Session 被正確的持久化。onResponseCommitted 方法如清單 14 所示:
清單 14. onResponseCommitted 方法
@Override protected void onResponseCommitted() { this.request.commitSession(); }
onResponseCommitted 方法本質上調用 SessionRepositoryRequestWrapper 的 commitSession 方法,如清單 15 所示: 清單 15. commitSession 方法
private void commitSession() { HttpSessionWrapper wrappedSession = getCurrentSession(); if (wrappedSession == null) { if (isInvalidateClientSession()) { SessionRepositoryFilter.this .httpSessionStrategy.onInvalidateSession(this, this.response); } } else { S session = wrappedSession.getSession(); SessionRepositoryFilter.this .sessionRepository.save(session); if(!isRequestedSessionIdValid()|| !session.getId().equals (getRequestedSessionId())){ SessionRepositoryFilter.this .httpSessionStrategy.onNewSession (session,this, this.response); } } }
commitSession 方法會判斷 Session 的狀態,進行失效、更新等處理。
MultiHttpSessionStrategy
MultiHttpSessionStrategy 繼承 RequestResponsePostProcessor 和 HttpSessionStrategy 接口。RequestResponsePostProcessor 接口,容許開發人員對 HttpServletRequest 和 HttpServletResponse 進行一些定製化的操做,例如讀取自定義的請求頭,進行個性化處理。
HttpSessionStrategy 即 Session 實現策略,上文提到 Session 的失效策略是採用 Cookie 的方式,所以 HttpSessionStrategy 的默認失效方式是 CookieHttpSessionStrategy。HttpSessionStrategy 的接口簽名如清單 16 所示:
清單 16. HttpSessionStrategy 方法
public interface HttpSessionStrategy { String getRequestedSessionId (HttpServletRequest request); void onNewSession (Session session, HttpServletRequest request ,HttpServletResponse response); void onInvalidateSession (HttpServletRequest request, HttpServletResponse response); }
如下是相關參數介紹:
getRequestedSessionId:獲取 Session 的 ID,默認從 Cookie 中獲取 Session 字段的值。
onNewSession:當用後臺爲請求創建了 Session 時,須要通知瀏覽器等客戶端,接收 Session 的 ID。默認經過 Cookie 實現,將 Session 字段填充 Session 的 ID,並放置在 Set-cookie 響應頭中。
onInvalidateSession:當 Session 失效時調用,默認經過 Cookie 的方式,將 Session 字段刪除。
下面簡單演示一下采用 Cookie 來實現 Session,如清單 17 所示:
清單 17. 採用 Cookie 實現 Session 示例
Request(請求) GET / HTTP/1.0 Host: kuboot.cn Response(響應) HTTP/1.0 200 OK Set-cookie: session=」123」; domain=」kuboot.cn」 Request(請求) GET / HTTP/1.0 Host: kuboot.cn Cookie: session=」123」
這是一個 demo,演示瞭如何簡單的使用 Spring Session 與 Web 進行集成。項目地址:q q q u n:948368769(大量資源q u n)
本文分析了 Spring Session 的架構,演示了採用 Redis 存儲 Session 的實現細節,涉及時間監聽和如何經過定時任務巧妙地失效 Session。此外,經過源碼解析梳理了在 Web 中集成 Spring Session 的流程。