sessionId 解析策略
SpringSession中對(duì)于sessionId的解析相關(guān)的策略是通過(guò)HttpSessionIdResolver這個(gè)接口來(lái)體現(xiàn)的。HttpSessionIdResolver有兩個(gè)實(shí)現(xiàn)類(lèi):
這兩個(gè)類(lèi)就分別對(duì)應(yīng)SpringSession解析sessionId的兩種不同的實(shí)現(xiàn)策略流强。再深入了解不同策略的實(shí)現(xiàn)細(xì)節(jié)之前痹届,先來(lái)看下HttpSessionIdResolver接口定義的一些行為有哪些。
HttpSessionIdResolver
HttpSessionIdResolver定義了sessionId解析策略的契約(Contract)打月。允許通過(guò)請(qǐng)求解析sessionId队腐,并通過(guò)響應(yīng)發(fā)送sessionId或終止會(huì)話(huà)。接口定義如下:
publicinterfaceHttpSessionIdResolver{ListresolveSessionIds(HttpServletRequest request);voidsetSessionId(HttpServletRequest request, HttpServletResponse response,String sessionId);voidexpireSession(HttpServletRequest request, HttpServletResponse response);}
HttpSessionIdResolver中有三個(gè)方法:
resolveSessionIds:解析與當(dāng)前請(qǐng)求相關(guān)聯(lián)的sessionId奏篙。sessionId可能來(lái)自Cookie或請(qǐng)求頭柴淘。
setSessionId:將給定的sessionId發(fā)送給客戶(hù)端。這個(gè)方法是在創(chuàng)建一個(gè)新session時(shí)被調(diào)用,并告知客戶(hù)端新sessionId是什么悠就。
expireSession:指示客戶(hù)端結(jié)束當(dāng)前session千绪。當(dāng)session無(wú)效時(shí)調(diào)用此方法充易,并應(yīng)通知客戶(hù)端sessionId不再有效梗脾。比如,它可能刪除一個(gè)包含sessionId的Cookie盹靴,或者設(shè)置一個(gè)HTTP響應(yīng)頭炸茧,其值為空就表示客戶(hù)端不再提交sessionId。
下面就針對(duì)上面提到的兩種策略來(lái)進(jìn)行詳細(xì)的分析稿静。
基于Cookie解析sessionId
這種策略對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)是CookieHttpSessionIdResolver梭冠,通過(guò)從Cookie中獲取session改备;具體來(lái)說(shuō),這個(gè)實(shí)現(xiàn)將允許使用CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)指定Cookie序列化策略悬钳。默認(rèn)的Cookie名稱(chēng)是“SESSION”。創(chuàng)建一個(gè)session時(shí)默勾,HTTP響應(yīng)中將會(huì)攜帶一個(gè)指定Cookie name且value是sessionId的Cookie。Cookie將被標(biāo)記為一個(gè)session cookie母剥,Cookie的domain path使用context path,且被標(biāo)記為HttpOnly环疼,如果HttpServletRequest#isSecure()返回true,那么Cookie將標(biāo)記為安全的炫隶。如下:
HTTP/1.1 200 OKSet-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
這個(gè)時(shí)候,客戶(hù)端應(yīng)該通過(guò)在每個(gè)請(qǐng)求中指定相同的Cookie來(lái)包含session信息等限。例如:
GET /messages/ HTTP/1.1 Host: example.com Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
當(dāng)會(huì)話(huà)無(wú)效時(shí)爸吮,服務(wù)器將發(fā)送過(guò)期的HTTP響應(yīng)Cookie,例如:
HTTP/1.1 200 OK Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Expires=Thur, 1 Jan 1970 00:00:00 GMT; Secure; HttpOnly
CookieHttpSessionIdResolver類(lèi)的實(shí)現(xiàn)如下:
publicfinalclassCookieHttpSessionIdResolverimplementsHttpSessionIdResolver{privatestaticfinalString WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class.getName().concat(".WRITTEN_SESSION_ID_ATTR");// Cookie序列化策略望门,默認(rèn)是 DefaultCookieSerializerprivateCookieSerializer cookieSerializer =newDefaultCookieSerializer();@OverridepublicListresolveSessionIds(HttpServletRequest request){// 根據(jù)提供的cookieSerializer從請(qǐng)求中獲取sessionIdreturnthis.cookieSerializer.readCookieValues(request);}@OverridepublicvoidsetSessionId(HttpServletRequest request, HttpServletResponse response,
String sessionId){if(sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {return;}request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);// 根據(jù)提供的cookieSerializer將sessionId回寫(xiě)到cookie中this.cookieSerializer.writeCookieValue(newCookieValue(request, response, sessionId));}@OverridepublicvoidexpireSession(HttpServletRequest request, HttpServletResponse response){// 這里因?yàn)槭沁^(guò)期形娇,所以回寫(xiě)的sessionId的值是“”,當(dāng)請(qǐng)求下次進(jìn)來(lái)時(shí)筹误,就會(huì)取不到sessionId桐早,也就意味著當(dāng)前會(huì)話(huà)失效了this.cookieSerializer.writeCookieValue(newCookieValue(request, response,""));}// 指定Cookie序列化的方式publicvoidsetCookieSerializer(CookieSerializer cookieSerializer){if(cookieSerializer ==null) {thrownewIllegalArgumentException("cookieSerializer cannot be null");}this.cookieSerializer = cookieSerializer;}}
這里可以看到CookieHttpSessionIdResolver中的讀取操作都是圍繞CookieSerializer來(lái)完成的。CookieSerializer是SpringSession中對(duì)于Cookie操作提供的一種機(jī)制。下面細(xì)說(shuō)哄酝。
基于請(qǐng)求頭解析sessionId
這種策略對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)是HeaderHttpSessionIdResolver友存,通過(guò)從請(qǐng)求頭header中解析出sessionId。具體地說(shuō)陶衅,這個(gè)實(shí)現(xiàn)將允許使用HeaderHttpSessionIdResolver(String)來(lái)指定頭名稱(chēng)屡立。還可以使用便利的工廠(chǎng)方法來(lái)創(chuàng)建使用公共頭名稱(chēng)(例如“X-Auth-Token”和“authenticing-info”)的實(shí)例。創(chuàng)建會(huì)話(huà)時(shí)搀军,HTTP響應(yīng)將具有指定名稱(chēng)和sessionId值的響應(yīng)頭膨俐。
// 使用X-Auth-Token作為headerNamepublicstaticHeaderHttpSessionIdResolverxAuthToken(){returnnewHeaderHttpSessionIdResolver(HEADER_X_AUTH_TOKEN);}// 使用Authentication-Info作為headerNamepublicstaticHeaderHttpSessionIdResolverauthenticationInfo(){returnnewHeaderHttpSessionIdResolver(HEADER_AUTHENTICATION_INFO);}
HeaderHttpSessionIdResolver在處理sessionId上相比較于CookieHttpSessionIdResolver來(lái)說(shuō)簡(jiǎn)單很多。就是圍繞request.getHeader(String)和request.setHeader(String,String)兩個(gè)方法來(lái)玩的罩句。
HeaderHttpSessionIdResolver這種策略通常會(huì)在無(wú)線(xiàn)端來(lái)使用焚刺,以彌補(bǔ)對(duì)于無(wú)Cookie場(chǎng)景的支持。
Cookie 序列化策略
基于Cookie解析sessionId的實(shí)現(xiàn)類(lèi)CookieHttpSessionIdResolver中實(shí)際對(duì)于Cookie的讀寫(xiě)操作都是通過(guò)CookieSerializer來(lái)完成的门烂。SpringSession提供了CookieSerializer接口的默認(rèn)實(shí)現(xiàn)DefaultCookieSerializer乳愉,當(dāng)然在實(shí)際應(yīng)用中,我們也可以自己實(shí)現(xiàn)這個(gè)接口屯远,然后通過(guò)CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)方法來(lái)指定我們自己的實(shí)現(xiàn)方式蔓姚。
PS:不得不說(shuō),強(qiáng)大的用戶(hù)擴(kuò)展能力真的是Spring家族的優(yōu)良家風(fēng)氓润。
jvm_router的處理
CookieValue
CookieValue是CookieSerializer中的內(nèi)部類(lèi)赂乐,封裝了向HttpServletResponse寫(xiě)入所需的所有信息。其實(shí)CookieValue的存在并沒(méi)有什么特殊的意義咖气,個(gè)人覺(jué)得作者一開(kāi)始只是想通過(guò)CookieValue的封裝來(lái)簡(jiǎn)化回寫(xiě)cookie鏈路中的參數(shù)傳遞的問(wèn)題挨措,但是實(shí)際上貌似并沒(méi)有什么減少多少工作量。
Cookie 回寫(xiě)
Cookie回寫(xiě)我覺(jué)得對(duì)于分布式session的實(shí)現(xiàn)來(lái)說(shuō)是必不可少的崩溪;基于標(biāo)準(zhǔn)servlet實(shí)現(xiàn)的HttpSession浅役,我們?cè)谑褂脮r(shí)實(shí)際上是不用關(guān)心回寫(xiě)cookie這個(gè)事情的,因?yàn)閟ervlet容器都已經(jīng)做了伶唯。但是對(duì)于分布式session來(lái)說(shuō)觉既,由于重寫(xiě)了response,所以需要在返回response時(shí)需要將當(dāng)前session信息通過(guò)cookie的方式塞到response中返回給客戶(hù)端-這就是Cookie回寫(xiě)乳幸。下面是DefaultCookieSerializer中回寫(xiě)Cookie的邏輯粹断,細(xì)節(jié)在代碼中通過(guò)注釋標(biāo)注出來(lái)。
@OverridepublicvoidwriteCookieValue(CookieValue cookieValue){HttpServletRequest request = cookieValue.getRequest();HttpServletResponse response = cookieValue.getResponse();StringBuilder sb =newStringBuilder();sb.append(this.cookieName).append('=');String value = getValue(cookieValue);if(value !=null&& value.length() >0) {validateValue(value);sb.append(value);}intmaxAge = getMaxAge(cookieValue);if(maxAge > -1) {sb.append("; Max-Age=").append(cookieValue.getCookieMaxAge());OffsetDateTime expires = (maxAge !=0)? OffsetDateTime.now().plusSeconds(maxAge): Instant.EPOCH.atOffset(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());}
這上面就是拼湊字符串希柿,然后塞到Header里面去,最終再瀏覽器中顯示大體如下:
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
jvm_router的處理
在Cookie的讀寫(xiě)代碼中都涉及到對(duì)于jvmRoute這個(gè)屬性的判斷及對(duì)應(yīng)的處理邏輯端姚。
1渐裸、讀取Cookie中的代碼片段
if(this.jvmRoute !=null&& sessionId.endsWith(this.jvmRoute)) {sessionId = sessionId.substring(0,sessionId.length() -this.jvmRoute.length());}
2尖啡、回寫(xiě)Cookie中的代碼片段
if(this.jvmRoute !=null) {actualCookieValue = requestedCookieValue +this.jvmRoute;}
jvm_route是Nginx中的一個(gè)模塊剩膘,其作用是通過(guò)session cookie的方式來(lái)獲取session粘性怠褐。如果在cookie和url中并沒(méi)有session,則這只是個(gè)簡(jiǎn)單的round-robin負(fù)載均衡奠涌。其具體過(guò)程分為以下幾步:
1.第一個(gè)請(qǐng)求過(guò)來(lái)磷杏,沒(méi)有帶session信息极祸,jvm_route就根據(jù)round robin策略發(fā)到一臺(tái)tomcat上面。
2.tomcat添加上session信息浴捆,并返回給客戶(hù)稿械。
3.用戶(hù)再次請(qǐng)求,jvm_route看到session中有后端服務(wù)器的名稱(chēng)页眯,它就把請(qǐng)求轉(zhuǎn)到對(duì)應(yīng)的服務(wù)器上窝撵。
從本質(zhì)上來(lái)說(shuō)述吸,jvm_route也是解決session共享的一種解決方式锣笨。這種和基于IP-HASH的方式有點(diǎn)類(lèi)似错英。那么同樣椭岩,這里存在的問(wèn)題是無(wú)法解決宕機(jī)后session數(shù)據(jù)轉(zhuǎn)移的問(wèn)題璃赡,既宕機(jī)就丟失。
DefaultCookieSerializer中除了Cookie的讀寫(xiě)之后塌计,還有一些細(xì)節(jié)也值得關(guān)注下侯谁,比如對(duì)Cookie中值的驗(yàn)證墙贱、remember-me的實(shí)現(xiàn)等。
在此我向大家推薦一個(gè)架構(gòu)學(xué)習(xí)交流群伊脓。交流學(xué)習(xí)群號(hào):938837867 暗號(hào):555 里面會(huì)分享一些資深架構(gòu)師錄制的視頻錄像:有Spring魁衙,MyBatis纺棺,Netty源碼分析,高并發(fā)茅撞、高性能巨朦、分布式、微服務(wù)架構(gòu)的原理拄查,JVM性能優(yōu)化、分布式架構(gòu)等這些成為架構(gòu)師必備