摘要:會(huì)話管理一直是企業(yè)級應(yīng)用的重要部分。傳統(tǒng)會(huì)話管理技術(shù)的問題的目的是解決傳統(tǒng)的會(huì)話管理技術(shù)的各種問題。對如和之類的閉源產(chǎn)品,找到適合它們的會(huì)話管理技術(shù)的替代實(shí)現(xiàn)則通常是不可能的。典型的應(yīng)用會(huì)將當(dāng)前用戶的身份及其安全級別或角色存儲(chǔ)在會(huì)話里面。
歡迎大家前往騰訊云+社區(qū),獲取更多騰訊海量技術(shù)實(shí)踐干貨哦~
本文來自云+社區(qū)翻譯社,由Tnecesoc編譯。
會(huì)話管理一直是 Java 企業(yè)級應(yīng)用的重要部分。不過在很長的一段時(shí)間里,這一部分都被我們認(rèn)為是一個(gè)已解決的問題,并且也沒有什么重大的創(chuàng)新出現(xiàn)。
然而,微服務(wù)還有可橫向伸縮的云原生應(yīng)用這一現(xiàn)代趨勢揭露了現(xiàn)今的會(huì)話管理技術(shù)在設(shè)計(jì)上的一些缺陷,挑戰(zhàn)著我們在過去 20 多年來對這一設(shè)計(jì)得出的一些結(jié)論。
本文會(huì)演示Spring Session API 為了幫助我們克服以前的會(huì)話管理方式的一些局限所采取的方法。我們將會(huì)先總結(jié)一下當(dāng)前的會(huì)話管理技術(shù)的問題,然后深入探討 Spring Session 解決這些問題所采取的策略。最后,我們會(huì)總結(jié) Spring Session 的工作方式以及在具體項(xiàng)目里面的一些用法。
Spring Session 為企業(yè)級 Java 應(yīng)用的會(huì)話管理領(lǐng)域帶來了革新,讓我們可以輕松做到:
編寫可橫向伸縮的云原生應(yīng)用
將會(huì)話狀態(tài)的存儲(chǔ)外放到專門的外部會(huì)話存儲(chǔ)里,比如 Redis 或 Apache Geode,后者以獨(dú)立于應(yīng)用程序服務(wù)器的方式提供了高質(zhì)量的存儲(chǔ)集群
在用戶通過 WebSocket 發(fā)出請求的時(shí)候保持 HttpSession 的在線狀態(tài)
訪問來自非 Web 請求處理指令的會(huì)話數(shù)據(jù),比如 JMS 消息處理指令
為每個(gè)瀏覽器建立多個(gè)會(huì)話提供支持,從而構(gòu)建更豐富的終端用戶體驗(yàn)
控制在客戶端和服務(wù)器間交換會(huì)話 ID 的方式,從而編寫在 HTTP 報(bào)文首部中提取會(huì)話 ID 而脫離對 Cookie 的依賴的 RESTul API
注意,Spring Session 項(xiàng)目其實(shí)并不依賴于 Spring 框架,因此我們甚至能在不使用 Spring 框架的項(xiàng)目里面用到它。
傳統(tǒng)會(huì)話管理技術(shù)的問題Spring Session 的目的是解決傳統(tǒng)的 JavaEE 會(huì)話管理技術(shù)的各種問題。下面就通過一些例子說明一些這方面的問題。
構(gòu)建可橫向伸縮的云原生應(yīng)用程序從云原生應(yīng)用架構(gòu)的視角來看,一個(gè)應(yīng)用應(yīng)該可以通過在一個(gè)大型的虛擬機(jī)池里運(yùn)行更多的 Linux 容器來部署更多的實(shí)例的方式來得到橫向的伸縮。比如,我們能很輕松地將一個(gè)這樣的應(yīng)用的 war 文件部署到 Cloud Foundry 或 Heroku 上的 Tomcat 里面,然后在幾秒內(nèi)擴(kuò)展出 100 個(gè)應(yīng)用程序?qū)嵗?,使得其中每個(gè)實(shí)例都有 1GB 的 RAM。我們還可以將云平臺(tái)設(shè)置成會(huì)根據(jù)用戶需求自動(dòng)增減應(yīng)用程序?qū)嵗臄?shù)量。
很多應(yīng)用都會(huì)把 HTTP 會(huì)話狀態(tài)存儲(chǔ)在運(yùn)行應(yīng)用代碼的 JVM 里面。這很容易實(shí)現(xiàn),而且存取的速度也很快。當(dāng)一個(gè)應(yīng)用實(shí)例加入或退出集群的時(shí)候,HTTP 會(huì)話的存儲(chǔ)會(huì)在所有尚存的應(yīng)用程序?qū)嵗现匦逻M(jìn)行平均的分配。在彈性云環(huán)境中,我們會(huì)運(yùn)行數(shù)以百計(jì)的應(yīng)用實(shí)例,且實(shí)例數(shù)量可能隨時(shí)發(fā)生快速的增減變化。這就帶來了一些問題:
HTTP 會(huì)話存儲(chǔ)的重新分配會(huì)成為性能瓶頸;
存儲(chǔ)大量會(huì)話所需的堆空間太大,會(huì)導(dǎo)致垃圾回收過程頻繁進(jìn)行,并影響性能;
TCP 組播通常會(huì)被云端的基礎(chǔ)架構(gòu)所禁止,但會(huì)話管理器需要經(jīng)常用它來發(fā)現(xiàn)加入或退出集群的應(yīng)用實(shí)例。
因此,將 HTTP 會(huì)話狀態(tài)存儲(chǔ)在運(yùn)行應(yīng)用代碼的 JVM 之外的數(shù)據(jù)存儲(chǔ)中會(huì)更高效。例如可以設(shè)置并使用 Redis 來存儲(chǔ)上述的 100 個(gè) Tomcat 實(shí)例里面的會(huì)話狀態(tài),那么 Tomcat 實(shí)例數(shù)量的增減便不會(huì)影響到在 Redis 中的會(huì)話存儲(chǔ)的模式。另外,因?yàn)?Redis 是用 C 語言編寫的,所以它可以在沒有垃圾回收機(jī)制影響其運(yùn)行的前提下,動(dòng)用數(shù)百 GB 甚至 TB 數(shù)量級的內(nèi)存。
對像 Tomcat 這樣的開源服務(wù)器,找到使用外部數(shù)據(jù)存儲(chǔ)(如 Redis 或 Memcached)的會(huì)話管理技術(shù)的其他實(shí)現(xiàn)是很簡單的,但是使用起來的配置過程可能很復(fù)雜,并且每個(gè)應(yīng)用服務(wù)器的配置過程可能都不一樣。對如 WebSphere 和 Weblogic 之類的閉源產(chǎn)品,找到適合它們的會(huì)話管理技術(shù)的替代實(shí)現(xiàn)則通常是不可能的。
Spring Session 為設(shè)置插件式的會(huì)話數(shù)據(jù)存儲(chǔ)提供了一種獨(dú)立于具體應(yīng)用服務(wù)器的方法,使得我們能在 Servlet 框架的范疇內(nèi)實(shí)現(xiàn)這樣的存儲(chǔ),而不用依賴于具體的應(yīng)用服務(wù)器的 API。這意味著 Spring Session 可以與所有實(shí)現(xiàn)了 Servlet 規(guī)范的應(yīng)用服務(wù)器(Tomcat,Jetty,WebSphere,WebLogic,JBoss)協(xié)同工作,并在所有應(yīng)用服務(wù)器上以完全相同且很容易的方式來進(jìn)行配置。
我們還可以根據(jù)我們的需求選用最適合的外部會(huì)話數(shù)據(jù)存儲(chǔ)。這使得 Spring Session 也成了一個(gè)能幫助我們將傳統(tǒng)的 JavaEE 應(yīng)用遷移到云端并作為一個(gè)符合十二要素的應(yīng)用的一個(gè)理想的遷移工具。
一個(gè)用戶,多個(gè)賬戶假設(shè)你正在 example.com 上運(yùn)行一個(gè)面向大眾的 Web 應(yīng)用,其中一些人類用戶創(chuàng)建了多個(gè)帳號(hào)。例如,用戶 Jeff Lebowski 可能有兩個(gè)帳號(hào) [email protected] 和 [email protected]。跟其他 Java Web 應(yīng)用程序一樣,你可以使用 HttpSession 來跟蹤各種會(huì)話狀態(tài),比如當(dāng)前登錄的用戶。因此,當(dāng)用戶想從 [email protected] 切換到 [email protected] 時(shí),就必須注銷當(dāng)前賬號(hào)并重新登錄。
使用 Spring Session 來為每個(gè)用戶配置多個(gè) HTTP 會(huì)話就很簡單了。這時(shí) Jeff Lebowski 無需注銷和登錄就可以在 [email protected] 和 [email protected] 之間來回切換。
不同安全級別下的預(yù)覽想象一下,你要構(gòu)建一個(gè)具有復(fù)雜的自定義授權(quán)體系的 Web 應(yīng)用,其中具有不同權(quán)限的用戶會(huì)具有不同的應(yīng)用 UI 樣式。
比如說,假設(shè)應(yīng)用有四個(gè)安全級別:公開(public)、保密(confidential)、機(jī)密(secret)以及絕密(top secret)。在用戶登錄到應(yīng)用時(shí),系統(tǒng)會(huì)識(shí)別這一用戶的安全級別,然后只對其顯示不高于其安全級別的數(shù)據(jù)。這樣,公開級別的用戶可以看到公開級別的文檔;具有保密級別的用戶能看公開和保密級別的,以此類推。為了讓用戶界面更加友好,我們的應(yīng)用也應(yīng)該能讓用戶預(yù)覽應(yīng)用的 UI 在較低的安全級別下的樣子。比如絕密級別用戶應(yīng)該能在秘密模式下預(yù)覽應(yīng)用的各項(xiàng)事物的外觀。
典型的 Web 應(yīng)用會(huì)將當(dāng)前用戶的身份及其安全級別或角色存儲(chǔ)在 HTTP 會(huì)話里面。不過,由于 Web 應(yīng)用的每個(gè)用戶只有一個(gè)會(huì)話,因此也只能通過注銷再登錄的方式來切換用戶的角色,或者實(shí)現(xiàn)一個(gè)用戶多個(gè)會(huì)話這一形式。
憑借 Spring Session,我們就可以很輕松地給每個(gè)登錄用戶創(chuàng)建多個(gè)相互獨(dú)立的會(huì)話,預(yù)覽功能的實(shí)現(xiàn)也會(huì)因此變得簡單。比如當(dāng)前以絕密等級登錄的用戶想要預(yù)覽機(jī)密等級下的應(yīng)用時(shí),就可以對其創(chuàng)建并使用一個(gè)新的安全級別為機(jī)密的會(huì)話。
在使用 Web Sockets 時(shí)保持登錄狀態(tài)再想象一個(gè)場景,在用戶通過 example.com 登錄到我們的 Web 應(yīng)用時(shí),他們能使用通過 Websockets 工作的一個(gè) HTML5 即時(shí)聊天客戶端進(jìn)行對話。不過,根據(jù) Servlet 規(guī)范,通過 Websockets 發(fā)出的請求不會(huì)更新會(huì)話的過期時(shí)間,因此在用戶進(jìn)行聊天的時(shí)候,無論他們的聊天有多頻繁,會(huì)話也可能聊著聊著就沒了,然后 Websocket 連接也會(huì)因此關(guān)閉,聊天也就無法繼續(xù)了。
又是憑借 Spring Session,我們可以很輕松地確保 Websocket 請求還有常規(guī)的 HTTP 請求都能更新會(huì)話的過期時(shí)間。
訪問對非 Web 請求的會(huì)話數(shù)據(jù)再想象一下,我們的應(yīng)用提供了兩種訪問方式,一個(gè)基于 HTTP 的 RESTful API,另一個(gè)是基于 RabbitMQ 的 AMQP 消息。此時(shí),執(zhí)行處理 AMQP 消息的的線程是無法訪問應(yīng)用服務(wù)器的 HttpSession 的,對此我們必須自己寫一個(gè)解決方案來訪問 HTTP 會(huì)話里邊的數(shù)據(jù)。
還是憑借 Spring Session,只要我們知道會(huì)話的 ID,就可以從應(yīng)用程序的任意線程訪問 Spring Session。Spring Session 比以往的 Servlet HTTP 會(huì)話管理器有著功能更加豐富的 API,使得我們只需要知道會(huì)話 ID 就能定位我們想要找的會(huì)話。比如,我們可以用傳入消息的用戶標(biāo)識(shí)字段來直接找到對應(yīng)的會(huì)話。
Spring Session 的工作方式現(xiàn)在傳統(tǒng)應(yīng)用服務(wù)器在 HTTP 會(huì)話管理方面的局限性已經(jīng)在不同情境中展示過了,我們再來看看 Spring Session 是如何解決這些問題的。
Spring Session 架構(gòu)在實(shí)現(xiàn)一個(gè)會(huì)話管理器的時(shí)候,有兩個(gè)關(guān)鍵問題必須得到解決:
如何創(chuàng)建一個(gè)高效、可靠、高可用的會(huì)話數(shù)據(jù)存儲(chǔ)集群?
如何確定能夠哪個(gè)會(huì)話的實(shí)例與哪個(gè)傳入的請求(形式有 HTTP、WebSocket、AMQP 等)相關(guān)聯(lián)?
不過在本質(zhì)上,有個(gè)更關(guān)鍵的問題是:如何跨越不同的請求協(xié)議來傳輸一個(gè)會(huì)話的 ID?
第一個(gè)問題對 Spring Session 來說已被各種高可用可伸縮的集群存儲(chǔ)(Redis、Gemfire、Apache Geode 等)很好地解決了。因此 Spring Session 也應(yīng)該定義一組標(biāo)準(zhǔn)接口來使得對底層數(shù)據(jù)存儲(chǔ)的訪問可以用不同的數(shù)據(jù)存儲(chǔ)來實(shí)現(xiàn)。Spring Session 在定義 Session 和 ExpiringSession 這些基本的關(guān)鍵接口之外,也針對了不同數(shù)據(jù)存儲(chǔ)的訪問定義了關(guān)鍵接口 SessionRepository。
org.springframework.session.Session 是定義會(huì)話基本功能的接口,例如屬性的設(shè)置和刪除。這個(gè)接口并不依賴于具體的底層技術(shù),因此可以比 Servlet 里面的 HttpSession 適用于更多的情況;
org.springframework.session.ExpiringSession 則擴(kuò)展了 Session 接口。它提供了一些屬性,讓我們可以設(shè)置具有時(shí)效性的會(huì)話,并查詢這個(gè)會(huì)話是否已經(jīng)過期。RedisSession 便是這個(gè)接口的一個(gè)實(shí)現(xiàn)范例。
org.springframework.session.SessionRepository 定義了創(chuàng)建,保存,刪除和查找會(huì)話的方法。將 Session 保存到數(shù)據(jù)存儲(chǔ)的實(shí)際邏輯便寫在這一接口的具體實(shí)現(xiàn)中。例如 RedisOperationsSessionRepository 便是這個(gè)接口的一個(gè)實(shí)現(xiàn),它使用 Redis 來實(shí)現(xiàn)了會(huì)話的創(chuàng)建、保存以及刪除。
至于將請求關(guān)聯(lián)到特定會(huì)話實(shí)例的問題,Spring Session 則假定這一關(guān)聯(lián)的過程取決于特定的協(xié)議,因?yàn)榭蛻舳撕头?wù)器在請求 / 響應(yīng)周期期間就需要對所傳輸?shù)臅?huì)話 ID 達(dá)成一致。比如,如果客戶端發(fā)來一個(gè) HTTP 請求,那么會(huì)話就可以通過 Cookie 或者 HTTP 報(bào)文首部來和請求相關(guān)聯(lián)。如果發(fā)來一個(gè) HTTPS 請求,則可用 SSL 的 Session ID 字段來講會(huì)話與請求相關(guān)聯(lián)。若發(fā)來的是 JMS 消息,那也可以用消息首部來存儲(chǔ)請求和響應(yīng)間的會(huì)話 ID。
對 HTTP 協(xié)議的關(guān)聯(lián)操作,Spring 會(huì)話定義了一個(gè) HttpSessionStrategy 接口,后者有將 Cookies 和會(huì)話關(guān)聯(lián)在一起的 CookieHttpSessionStrategy 和使用了自定義報(bào)文首部字段來管理會(huì)話的 HeaderHttpSessionStrategy 兩種實(shí)現(xiàn)。
下面便詳細(xì)地介紹一下 Spring Session 在 HTTP 協(xié)議上的工作方式。
在本文發(fā)布時(shí)(2015.11.10),Spring Session 1.0.2 在當(dāng)前的 GA 發(fā)行版提供了使用 Redis 的 Spring Session 的一套實(shí)現(xiàn),以及支持任何分布式的 Map(如 Hazelcast)的實(shí)現(xiàn)。其實(shí),實(shí)現(xiàn) Spring Session 針對某種數(shù)據(jù)存儲(chǔ)的支持是相對容易的,在開源社區(qū)里已經(jīng)有了很多這樣的實(shí)現(xiàn)。
基于 HTTP 的 Spring Session基于 HTTP 的 Spring Session 是以一個(gè)標(biāo)準(zhǔn) Servlet 過濾器(filter)的形式實(shí)現(xiàn)的。這一過濾器應(yīng)該截取所有的對 Web 應(yīng)用的請求,并且也應(yīng)該在諸多過濾器組成的鏈中排在第一個(gè)。Spring Session 的過濾器會(huì)負(fù)責(zé)確保所有后續(xù)的代碼里面對 javax.servlet.http.HttpServletRequest.getSession() 方法的調(diào)用都會(huì)呈遞給一個(gè) Spinrg Session 的 HttpSession 實(shí)例,而不是應(yīng)用服務(wù)器默認(rèn)提供的 HttpSession。
要理解這點(diǎn),最簡單的方法就是查閱 Spring Session 的實(shí)際源碼。我們首先從用來實(shí)現(xiàn) Spring Session 的標(biāo)準(zhǔn) Servlet 擴(kuò)展點(diǎn)(extension points)開始。
在 2001 年,Servlet 2.3 規(guī)范引入了 ServletRequestWrapper。該類的 Javadoc 稱 ServletRequestWrapper “為 ServletRequest 接口能讓開發(fā)者繼承它來適配一種特別的 Servlet 提供了一種便利的實(shí)現(xiàn)。該類采用了包裝器,或者說裝飾器模式。對該類的 ServletRequest 類的方法的調(diào)用會(huì)被傳至其封裝的一個(gè)請求對象里去?!?下面這段從 Tomcat 里抽出來的代碼就展示了 ServletRequestWrapper 的實(shí)現(xiàn)方式。
public class ServletRequestWrapper implements ServletRequest { private ServletRequest request; /** * Creates a ServletRequest adaptor wrapping the given request object. * 創(chuàng)建一個(gè)裝有給定的請求對象的 ServletRequest 適配器 * @throws java.lang.IllegalArgumentException if the request is null * 如果請求對象為空就會(huì)拋出空指針異常 */ public ServletRequestWrapper(ServletRequest request) { if (request == null) { throw new IllegalArgumentException("Request cannot be null"); } this.request = request; } public ServletRequest getRequest() { return this.request; } public Object getAttribute(String name) { return this.request.getAttribute(name); } // 為可讀性著想, 接下來的代碼就略了 }
Servlt 2.3 規(guī)范還對 ServletRequestWrapper 定義了一個(gè)子類 HttpServletRequestWrapper。我們可以用它來快速地實(shí)現(xiàn)一個(gè)自定義的 HttpServletRequest。下面這段從 Tomcat 里抽出來的代碼就展示了 HttpServletRequestWrapper 這個(gè)類的實(shí)現(xiàn)方式。
public class HttpServletRequestWrapper extends ServletRequestWrapper implements HttpServletRequest { public HttpServletRequestWrapper(HttpServletRequest request) { super(request); } private HttpServletRequest _getHttpServletRequest() { return (HttpServletRequest) super.getRequest(); } public HttpSession getSession(boolean create) { return this._getHttpServletRequest().getSession(create); } public HttpSession getSession() { return this._getHttpServletRequest().getSession(); } // 為可讀性著想,接下來的代碼就略了 }
因此,我們就可以用這些包裝類來編寫一些擴(kuò)展 HttpServletRequest 功能的代碼,重載返回 HttpSession 的方法,使得后者返回的是我們存儲(chǔ)在外部存儲(chǔ)倉庫里面的會(huì)話。這里就給出一份從 Spring Session 項(xiàng)目提出來的源碼就對應(yīng)了這里提到的東西。為了能對應(yīng)這里的解釋,源碼里面原本的注釋被我重寫了一下,在此不妨也看一看里面的注釋。
/* * Spring Session 項(xiàng)目定義了一個(gè)繼承了標(biāo)準(zhǔn) HttpServletRequestWrapper 的類 * 它重載了 HttpServletRequest 里面的所有跟會(huì)話有關(guān)的方法 */ private final class SessionRepositoryRequestWrapper extends HttpServletRequestWrapper { private HttpSessionWrapper currentSession; private Boolean requestedSessionIdValid; private boolean requestedSessionInvalidated; private final HttpServletResponse response; private final ServletContext servletContext; /* * 構(gòu)造方法這塊非常簡單 * 它會(huì)接收并設(shè)置一些之后會(huì)用到的參數(shù), * 然后完成對 HttpServletRequestWrapper 的代理 */ private SessionRepositoryRequestWrapper( HttpServletRequest request, HttpServletResponse response, ServletContext servletContext) { super(request); this.response = response; this.servletContext = servletContext; } /* * Spring Session 便在這里用自己對返回存儲(chǔ)于外部數(shù)據(jù)源的會(huì)話數(shù)據(jù)的實(shí)現(xiàn) * 取代了對應(yīng)用服務(wù)器提供的默認(rèn)方法的代理調(diào)用. * * 這里的實(shí)現(xiàn)會(huì)先檢查它是不是已經(jīng)有一個(gè)對應(yīng)的會(huì)話. * 若有那就返回之, 否則就會(huì)檢查當(dāng)前的請求附帶的會(huì)話 ID 是否確實(shí)對應(yīng)著一個(gè)會(huì)話 * 若有, 那就用這個(gè)會(huì)話 ID 從 SessionRepository 里邊加載這個(gè)會(huì)話; * 若外部數(shù)據(jù)源里沒這個(gè)會(huì)話, 或者這個(gè)會(huì)話 ID 沒對應(yīng)的會(huì)話, * 那就創(chuàng)建一個(gè)新的會(huì)話, 并把它存在會(huì)話數(shù)據(jù)存儲(chǔ)里面. */ @Override public HttpSession getSession(boolean create) { if(currentSession != null) { return currentSession; } String requestedSessionId = getRequestedSessionId(); if(requestedSessionId != null) { S session = sessionRepository.getSession(requestedSessionId); if(session != null) { this.requestedSessionIdValid = true; currentSession = new HttpSessionWrapper(session, getServletContext()); currentSession.setNew(false); return currentSession; } } if(!create) { return null; } S session = sessionRepository.createSession(); currentSession = new HttpSessionWrapper(session, getServletContext()); return currentSession; } @Override public HttpSession getSession() { return getSession(true); } }
Spring Session 同時(shí)定義了一個(gè) ServletFilter 接口的實(shí)現(xiàn)類 SessionRepositoryFilter。這里也會(huì)給出這個(gè)過濾器的實(shí)現(xiàn)的核心部分的源碼,并且也會(huì)附上一些對應(yīng)本文內(nèi)容的注釋,不妨也看一看。
/* * SessionRepositoryFilter 是一個(gè)標(biāo)準(zhǔn) ServletFilter 的實(shí)現(xiàn). * 其目的是從它的基類擴(kuò)展出一些功能來. */ public class SessionRepositoryFilter < S extends ExpiringSession > extends OncePerRequestFilter { /* * 這一方法就是核心部分. * 該方法會(huì)創(chuàng)建一個(gè)我們在上面介紹過的包裝請求的實(shí)例, * 然后拿這個(gè)包裝過的請求再過一遍過濾器鏈的剩余部分. * 關(guān)鍵的地方在于,應(yīng)用在執(zhí)行位于這個(gè)過濾器之后的代碼時(shí), * 如果要獲取會(huì)話的數(shù)據(jù), 那這個(gè)包裝過的請求就會(huì)返回 Spring Session * 所保存在外部數(shù)據(jù)源的 HttpServletSession 實(shí)例. */ protected void doFilterInternal( HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { request.setAttribute(SESSION_REPOSITORY_ATTR, sessionRepository); SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request,response,servletContext); SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response); HttpServletRequest strategyRequest = httpSessionStrategy.wrapRequest(wrappedRequest, wrappedResponse); HttpServletResponse strategyResponse = httpSessionStrategy.wrapResponse(wrappedRequest, wrappedResponse); try { filterChain.doFilter(strategyRequest, strategyResponse); } finally { wrappedRequest.commitSession(); } } }
這一節(jié)的重點(diǎn)在于,基于 HTTP 的 Spring Session 其實(shí)也只是一個(gè)用了 Servlet 規(guī)范的標(biāo)準(zhǔn)特性來實(shí)現(xiàn)功能的經(jīng)典的 Servlet 過濾器而已。因此,將現(xiàn)有的 Web 應(yīng)用的 war 文件改成使用 Spring Session 是應(yīng)該可以不用改動(dòng)已有代碼的。然而,在應(yīng)用里面用了 javax.servlet.http.HttpSessionListener 的情況則是例外。Spring Session 1.0 并沒有對 HttpSessionListener 提供支持,不過 Spring Session 1.1 M1 版本則對其添加了支持。詳情見此。
Spring Session 的設(shè)置在 Web 項(xiàng)目里面,Spring Session 的設(shè)置分為四步:
設(shè)置在 Spring Session 中使用的數(shù)據(jù)存儲(chǔ)
將 Spring Session 的 .jar 文件添加到 Web 應(yīng)用中
將 Spring Session 的過濾器添加到 Web 應(yīng)用的配置中
設(shè)置從 Spring Session 到所選會(huì)話數(shù)據(jù)存儲(chǔ)的連接
Spring Session 內(nèi)置了對 Redis 的支持。安裝和設(shè)置 redis 的詳細(xì)信息見此。
完成上述 Spring Session 的設(shè)置步驟的常見方式有兩種。一種是使用 Spring Boot 來自動(dòng)設(shè)置 Spring Session。另外一種則是手動(dòng)完成每一個(gè)配置步驟。
用 Maven 和 Gradle 等依賴管理工具可以很輕松地將 Spring Session 加入到應(yīng)用的依賴項(xiàng)目里面。比如說,如果你用的是 Spring Boot + Maven,那么就可以在 pom.xml 里面加上以下依賴項(xiàng)目:
org.springframework.session spring-session 1.0.2.RELEASE org.springframework.boot spring-boot-starter-redis
spring-boot-starter-redis 這一依賴項(xiàng)目會(huì)確保跟 redis 交互所需的 jar 包都包含在應(yīng)用里面,這樣便可以使用 Spring Boot 來進(jìn)行自動(dòng)的配置。至于 spring-session 這一依賴項(xiàng)目則對應(yīng) Spring Session 的 jar 包。
設(shè)置 Spring Session Servlet 過濾器的過程可以通過 Spring Boot 自動(dòng)完成,只需要在 Spring Boot 的配置類里面加上 @EnableRedisHttpSession 注解即可。就跟下面這段代碼一樣:
@SpringBootApplication @EnableRedisHttpSession public class ExampleApplication { public static void main(String[] args) { SpringApplication.run(ExampleApplication.class, args); } }
將下面這些配置信息加到 Spring Boot 的 application.properties 文件即可設(shè)置 Spring Session 到 Redis 的連接。
spring.redis.host=localhost spring.redis.password=secret spring.redis.port=6379
為了設(shè)置和 Redis 的連接,Spring Boot 提供了一套詳實(shí)的底層架構(gòu),使得我們可以在其中任意設(shè)置一種跟 Redis 建立連接的方式。你能在 Spring Session 還有 Spring Boot 里面找到按部就班進(jìn)行的指南。
使用 web.xml 來設(shè)置傳統(tǒng)的 Web 應(yīng)用去使用 Spring Session 的教程見此。
設(shè)置傳統(tǒng)的不帶有 web.xml 的 war 文件去使用 Spring Session 的教程見此。
在默認(rèn)情況下,Spring Session 會(huì)使用 HTTP cookie 來存儲(chǔ)會(huì)話 ID,但是我們也可以將 Spring Session 設(shè)置成使用自定義的 HTTP 報(bào)文首部字段(例如 x-auth-token: 0dc1f6e1-c7f1-41ac-8ce2-32b6b3e57aa3
)來存儲(chǔ)會(huì)話 ID,而這在構(gòu)建 RESTful API 的時(shí)候會(huì)非常有用。完整教程見此。
Spring Session 的用法在配置了 Spring Session 之后,我們就可以使用標(biāo)準(zhǔn)的 Servlet API 去和它進(jìn)行交互了。比如下面這段代碼就定義了一個(gè)使用標(biāo)準(zhǔn) Servlet 會(huì)話 API 來訪問會(huì)話數(shù)據(jù)的 servlet。
@WebServlet("/example") public class Example extends HttpServlet { @Override protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 使用標(biāo)準(zhǔn)的 servlet API 去獲取對應(yīng)的會(huì)話數(shù)據(jù) // 這一會(huì)話數(shù)據(jù)就是 Spring Session 存在 Redis // 或是別的我們所指定的數(shù)據(jù)源里面的會(huì)話數(shù)據(jù) HttpSession session = request.getSession(); String value = session.getAttribute(a€?someAttributea€?); } }一個(gè)瀏覽器,多個(gè)會(huì)話
Spring Session 通過使用一個(gè)叫做 _s 的會(huì)話代號(hào)參數(shù)來跟蹤每個(gè)用戶的多個(gè)會(huì)話。假如有個(gè)傳入請求的 URL 是 http://example.com/doSomething?_s=0,那么 Spring Session 就會(huì)讀取 _s 參數(shù)的值,然后便會(huì)認(rèn)為這個(gè)請求對應(yīng)的是默認(rèn)的會(huì)話。
如果傳入請求的 URL 是 http://example.com/doSomething?_s=1,那么 Spring Session 就會(huì)知道這個(gè)請求對應(yīng)的會(huì)話的代號(hào)是 1。如果傳入請求沒有指定參數(shù) _s,那么 Spring Session 就會(huì)把它視為對應(yīng)默認(rèn)對話(即 _s = 0)。
為了讓每個(gè)瀏覽器都創(chuàng)建一個(gè)新的會(huì)話,我們只需像以前那樣調(diào)用 javax.servlet.http.HttpServletRequest.getSession(),然后 Spring Session 就會(huì)返回對應(yīng)的會(huì)話,或者使用 Servlet 規(guī)范的語義創(chuàng)建一個(gè)新的會(huì)話。下表便給出了 getSession() 方法在同一瀏覽器的不同的 URL 參數(shù)下的具體表現(xiàn)形式:
HTTP 請求 URL | 會(huì)話代號(hào) | getSession() 的具體表現(xiàn) |
---|---|---|
example.com/resource | 0 | 如果存在與代號(hào) 0 相關(guān)聯(lián)的會(huì)話就返回之,否則就創(chuàng)建一個(gè)新會(huì)話,然后將其與代號(hào) 0 關(guān)聯(lián)起來 |
example.com/resource?_s=1 | 1 | 如果存在與代號(hào) 1 相關(guān)聯(lián)的會(huì)話就返回之,否則就創(chuàng)建一個(gè)新會(huì)話,然后將其與代號(hào) 1 關(guān)聯(lián)起來 |
example.com/resource?_s=0 | 0 | 如果存在與代號(hào) 0 相關(guān)聯(lián)的會(huì)話就返回之,否則就創(chuàng)建一個(gè)新會(huì)話,然后將其與代號(hào) 0 關(guān)聯(lián)起來 |
example.com/resource?_s=abc | abc | 如果存在與代號(hào) abc 相關(guān)聯(lián)的會(huì)話就返回之,否則就創(chuàng)建一個(gè)新會(huì)話,然后將其與代號(hào) abc 關(guān)聯(lián)起來 |
如上表所示,會(huì)話代號(hào)并不局限于整數(shù),只要與發(fā)布給該用戶的所有其他會(huì)話別名不同,即可對一個(gè)一個(gè)新的會(huì)話。然而,整數(shù)類型的會(huì)話代號(hào)應(yīng)該是最易用的,并且 Spring Session 也給出了 HttpSessionManager 來提供一些處理會(huì)話代號(hào)的實(shí)用方法。
我們可以通過 "org.springframework.session.web.HttpSessionManager" 這個(gè)屬性名來查找相應(yīng)屬性,進(jìn)而訪問到 HttpSessionManager。下面這段代碼就演示了獲得 HttpSessionManager 的引用的方法,以及這個(gè)實(shí)用方法類的一些主要的方法。
@WebServlet("/example") public class Example extends HttpServlet { @Override protected void doGet(HttpServletRequest request,HttpServletResponse response) throws ServletException, IOException { /* * 通過使用 "org.springframework.session.web.http.HttpSessionManager" * 這一屬性名在請求屬性中查找屬性 * 來獲取一個(gè) Spring Session 的 HttpSessionManager 的引用 */ HttpSessionManager sessionManager=(HttpSessionManager)request.getAttribute( "org.springframework.session.web.http.HttpSessionManager"); /* * 用 HttpSessionManager 來找出 HTTP 請求所對應(yīng)的會(huì)話代號(hào). * 默認(rèn)情況下這個(gè)會(huì)話代號(hào)會(huì)由 HTTP 請求的 URL 參數(shù) _s 給出。 * 比如 http://localhost:8080/example?_s=1 這個(gè) URL * 就會(huì)讓這里的 println() 方法打印 "Requested Session Alias is: 1" */ String requestedSessionAlias=sessionManager.getCurrentSessionAlias(request); System.out.println("Requested Session Alias is: " + requestedSessionAlias); /* * 返回一個(gè)當(dāng)前還沒被瀏覽器用在請求參數(shù)里的唯一的會(huì)話代號(hào). * 注意這一方法并不會(huì)創(chuàng)建一個(gè)新的會(huì)話, * 創(chuàng)建新的會(huì)話還是要通過 request.getSession() 來進(jìn)行. */ String newSessionAlias = sessionManager.getNewSessionAlias(request); /* * 使用剛剛得到的新會(huì)話代號(hào)構(gòu)造一個(gè) URL, * 使其含有 _s 這個(gè)參數(shù). * 比如若 newSessionAlias 的值是 2, * 那么這個(gè)方法就會(huì)返回 "/inbox?_s=3" */ String encodedURL = sessionManager.encodeURL("/inbox", newSessionAlias); System.out.println(encodedURL); /* * 返回一個(gè)會(huì)話代號(hào)為鍵, 會(huì)話 ID 為值的 Map, * 以便識(shí)別瀏覽器發(fā)來的請求所對應(yīng)的會(huì)話. */ Map結(jié)論sessionIds = sessionManager.getSessionIds(request); } }
Spring Session 為企業(yè)級 Java 應(yīng)用的會(huì)話管理領(lǐng)域帶來了革新,讓我們可以輕松做到:
編寫可橫向伸縮的云原生應(yīng)用
將會(huì)話狀態(tài)的存儲(chǔ)外放到專門的外部會(huì)話存儲(chǔ)里,比如 Redis 或 Apache Geode,后者以獨(dú)立于應(yīng)用程序服務(wù)器的方式提供了高質(zhì)量的存儲(chǔ)集群
在用戶通過 WebSocket 發(fā)出請求的時(shí)候保持 HttpSession 的在線狀態(tài)
訪問來自非 Web 請求處理指令的會(huì)話數(shù)據(jù),比如 JMS 消息處理指令
為每個(gè)瀏覽器建立多個(gè)會(huì)話提供支持,從而構(gòu)建更豐富的終端用戶體驗(yàn)
控制在客戶端和服務(wù)器間交換會(huì)話 ID 的方式,從而編寫在 HTTP 報(bào)文首部中提取會(huì)話 ID 而脫離對 Cookie 的依賴的 RESTul API
若你在尋找一種從傳統(tǒng)又笨重的應(yīng)用服務(wù)器中解放的方法,但又囿于對應(yīng)用服務(wù)器的會(huì)話存儲(chǔ)集群功能的依賴,那么 Spring Session 對像 Tomcat、Jetty 還有 Undertow 這樣的容器的輕量化來說是很好的一個(gè)選擇。
參考資料Spring Session 項(xiàng)目
Spring Session 教程及指南
HttpSession & Redis
Spring Boot 集成
Spring Security 集成
Restful APIs
用戶多重賬號(hào)
Web Socket集成
Websocket / HttpSession 超時(shí)交互
ASF Bugzilla - Bug 54738
WEBSOCKET SPEC-175
網(wǎng)絡(luò)研討會(huì):Spring Session 導(dǎo)論
問答
傳統(tǒng)Web應(yīng)用程序和API中的身份驗(yàn)證、授權(quán)和會(huì)話管理如何實(shí)現(xiàn)?
相關(guān)閱讀
架構(gòu)設(shè)計(jì)之Spring Session分布式集群會(huì)話管理?
Spring Session關(guān)鍵類源碼分析
一個(gè)可以把web表單變成會(huì)話形式的開源框架
此文已由作者授權(quán)騰訊云+社區(qū)發(fā)布,原文鏈接:https://cloud.tencent.com/dev...
歡迎大家前往騰訊云+社區(qū)或關(guān)注云加社區(qū)微信公眾號(hào)(QcloudCommunity),第一時(shí)間獲取更多海量技術(shù)實(shí)踐干貨哦~
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/71410.html
摘要:從源碼的角度分析源碼分析從哪一步作為入口呢如果是看過我之前寫的那幾篇關(guān)于的源碼分析,我相信你不會(huì)在源碼前磨磨蹭蹭,遲遲找不到入口。 微信公眾號(hào)「后端進(jìn)階」,專注后端技術(shù)分享:Java、Golang、WEB框架、分布式中間件、服務(wù)治理等等。 老司機(jī)傾囊相授,帶你一路進(jìn)階,來不及解釋了快上車! 坐在我旁邊的鐘同學(xué)聽說我精通Mybatis源碼(我就想不通,是誰透漏了風(fēng)聲),就順帶問了我一個(gè)...
摘要:大家好,今天給大家分享一個(gè)權(quán)限管理的框架的,說實(shí)話本來我是準(zhǔn)備看的,畢竟是家族的框架,和整合更加容易一些。官方給出的介紹是是一個(gè)強(qiáng)大且易用的安全框架執(zhí)行身份驗(yàn)證授權(quán)密碼學(xué)和會(huì)話管理。由此可知,的主要功能是認(rèn)證授權(quán)加密密和會(huì)話管理。 showImg(https://segmentfault.com/img/bV1BsT?w=1726&h=256); 大家好,今天給大家分享一個(gè)權(quán)限管理的框...
摘要:大家好,今天給大家分享一個(gè)權(quán)限管理的框架的,說實(shí)話本來我是準(zhǔn)備看的,畢竟是家族的框架,和整合更加容易一些。官方給出的介紹是是一個(gè)強(qiáng)大且易用的安全框架執(zhí)行身份驗(yàn)證授權(quán)密碼學(xué)和會(huì)話管理。由此可知,的主要功能是認(rèn)證授權(quán)加密密和會(huì)話管理。 showImg(https://segmentfault.com/img/bV1BsT?w=1726&h=256); 大家好,今天給大家分享一個(gè)權(quán)限管理的框...
閱讀 3859·2023-01-11 11:02
閱讀 4350·2023-01-11 11:02
閱讀 3183·2023-01-11 11:02
閱讀 5283·2023-01-11 11:02
閱讀 4838·2023-01-11 11:02
閱讀 5648·2023-01-11 11:02
閱讀 5438·2023-01-11 11:02
閱讀 4162·2023-01-11 11:02