首先需求在這裡說明下,SpringSession的版本迭代的過程中肯定會伴隨著一些類的移除和一些類的加入,目前本系列使用的版本是github上物件的master的程式碼流版本。如果有同學對其他版本中的一些類或者處理有疑惑,歡迎交流。
本篇將來介紹下SpringSession
中兩種sessionId
解析的策略,這個在之前的文章中其實是有提到過的,這裡再拿出來和SpringSession
中Cookie
相關策略一起學習
下。
sessionId 解析策略
SpringSession
中對於sessionId
的解析相關的策略是通過HttpSessionIdResolver
這個介面來體現的。HttpSessionIdResolver
有兩個實現類:
這兩個類就分別對應SpringSession
解析sessionId
的兩種不同的實現策略。再深入瞭解不同策略的實現細節之前,先來看下HttpSessionIdResolver
介面定義的一些行為有哪些。
HttpSessionIdResolver
HttpSessionIdResolver
定義了sessionId
解析策略的契約(Contract
)。允許通過請求解析sessionId,並通過響應傳送sessionId或終止會話。介面定義如下:
public interface HttpSessionIdResolver {
List<String> resolveSessionIds(HttpServletRequest request);
void setSessionId(HttpServletRequest request, HttpServletResponse response,String sessionId);
void expireSession(HttpServletRequest request, HttpServletResponse response);
}
複製程式碼
HttpSessionIdResolver
中有三個方法:
resolveSessionIds
:解析與當前請求相關聯的sessionId
。sessionId
可能來自Cookie
或請求頭。setSessionId
:將給定的sessionId
傳送給客戶端。這個方法是在建立一個新session
時被呼叫,並告知客戶端新sessionId
是什麼。expireSession
:指示客戶端結束當前session
。當session
無效時呼叫此方法,並應通知客戶端sessionId
不再有效。比如,它可能刪除一個包含sessionId
的Cookie
,或者設定一個HTTP
響應頭,其值為空就表示客戶端不再提交sessionId
。
下面就針對上面提到的兩種策略來進行詳細的分析。
基於Cookie解析sessionId
這種策略對應的實現類是CookieHttpSessionIdResolver
,通過從Cookie
中獲取session
;具體來說,這個實現將允許使用CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)
指定Cookie
序列化策略。預設的Cookie
名稱是“SESSION
”。建立一個session
時,HTTP
響應中將會攜帶一個指定 Cookie name
且value
是sessionId
的Cookie
。Cookie
將被標記為一個 session cookie
,Cookie
的 domain path
使用 context path
,且被標記為HttpOnly
,如果HttpServletRequest#isSecure()
返回true
,那麼Cookie
將標記為安全的。如下:
關於
Cookie
,可以參考:聊一聊session和cookie。
HTTP/1.1 200 OK
Set-Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6; Path=/context-root; Secure; HttpOnly
複製程式碼
這個時候,客戶端應該通過在每個請求中指定相同的Cookie
來包含session
資訊。例如:
GET /messages/ HTTP/1.1
Host: example.com
Cookie: SESSION=f81d4fae-7dec-11d0-a765-00a0c91e6bf6
複製程式碼
當會話無效時,伺服器將傳送過期的HTTP
響應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
類的實現如下:
public final class CookieHttpSessionIdResolver implements HttpSessionIdResolver {
private static final String WRITTEN_SESSION_ID_ATTR = CookieHttpSessionIdResolver.class
.getName().concat(".WRITTEN_SESSION_ID_ATTR");
// Cookie序列化策略,預設是 DefaultCookieSerializer
private CookieSerializer cookieSerializer = new DefaultCookieSerializer();
@Override
public List<String> resolveSessionIds(HttpServletRequest request) {
// 根據提供的cookieSerializer從請求中獲取sessionId
return this.cookieSerializer.readCookieValues(request);
}
@Override
public void setSessionId(HttpServletRequest request, HttpServletResponse response,
String sessionId) {
if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
return;
}
request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
// 根據提供的cookieSerializer將sessionId回寫到cookie中
this.cookieSerializer
.writeCookieValue(new CookieValue(request, response, sessionId));
}
@Override
public void expireSession(HttpServletRequest request, HttpServletResponse response) {
// 這裡因為是過期,所以回寫的sessionId的值是“”,當請求下次進來時,就會取不到sessionId,也就意味著當前會話失效了
this.cookieSerializer.writeCookieValue(new CookieValue(request, response, ""));
}
// 指定Cookie序列化的方式
public void setCookieSerializer(CookieSerializer cookieSerializer) {
if (cookieSerializer == null) {
throw new IllegalArgumentException("cookieSerializer cannot be null");
}
this.cookieSerializer = cookieSerializer;
}
}
複製程式碼
這裡可以看到CookieHttpSessionIdResolver
中的讀取操作都是圍繞CookieSerializer
來完成的。CookieSerializer
是SpringSession
中對於Cookie
操作提供的一種機制。下面細說。
基於請求頭解析sessionId
這種策略對應的實現類是HeaderHttpSessionIdResolver
,通過從請求頭header
中解析出sessionId
。具體地說,這個實現將允許使用HeaderHttpSessionIdResolver(String)
來指定頭名稱。還可以使用便利的工廠方法來建立使用公共頭名稱(例如“X-Auth-Token”
和“authenticing-info”
)的例項。建立會話時,HTTP
響應將具有指定名稱和sessionId
值的響應頭。
// 使用X-Auth-Token作為headerName
public static HeaderHttpSessionIdResolver xAuthToken() {
return new HeaderHttpSessionIdResolver(HEADER_X_AUTH_TOKEN);
}
// 使用Authentication-Info作為headerName
public static HeaderHttpSessionIdResolver authenticationInfo() {
return new HeaderHttpSessionIdResolver(HEADER_AUTHENTICATION_INFO);
}
複製程式碼
HeaderHttpSessionIdResolver
在處理sessionId
上相比較於CookieHttpSessionIdResolver
來說簡單很多。就是圍繞request.getHeader(String)
和request.setHeader(String,String)
兩個方法來玩的。
HeaderHttpSessionIdResolver
這種策略通常會在無線端來使用,以彌補對於無Cookie
場景的支援。
Cookie 序列化策略
基於Cookie
解析sessionId
的實現類CookieHttpSessionIdResolver
中實際對於Cookie
的讀寫操作都是通過CookieSerializer
來完成的。SpringSession
提供了CookieSerializer
介面的預設實現DefaultCookieSerializer
,當然在實際應用中,我們也可以自己實現這個介面,然後通過CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)
方法來指定我們自己的實現方式。
PS:不得不說,強大的使用者擴充套件能力真的是
Spring
家族的優良家風。
篇幅有限,這裡就只看下兩個點:
CookieValue
存在的意義是什麼DefaultCookieSerializer
回寫Cookie
的的具體實現,讀Cookie
在 SpringSession系列-請求與響應重寫 這篇文章中有介紹過,這裡不再贅述。- jvm_router的處理
CookieValue
CookieValue
是CookieSerializer
中的內部類,封裝了向HttpServletResponse
寫入所需的所有資訊。其實CookieValue
的存在並沒有什麼特殊的意義,個人覺得作者一開始只是想通過CookieValue
的封裝來簡化回寫cookie
鏈路中的引數傳遞的問題,但是實際上貌似並沒有什麼減少多少工作量。
Cookie 回寫
Cookie
回寫我覺得對於分散式session
的實現來說是必不可少的;基於標準servlet
實現的HttpSession
,我們在使用時實際上是不用關心回寫cookie
這個事情的,因為servlet
容器都已經做了。但是對於分散式session
來說,由於重寫了response
,所以需要在返回response
時需要將當前session
資訊通過cookie
的方式塞到response
中返回給客戶端-這就是Cookie
回寫。下面是DefaultCookieSerializer
中回寫Cookie
的邏輯,細節在程式碼中通過註釋標註出來。
@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());
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
的讀寫程式碼中都涉及到對於jvmRoute
這個屬性的判斷及對應的處理邏輯。
1、讀取Cookie
中的程式碼片段
if (this.jvmRoute != null && sessionId.endsWith(this.jvmRoute)) {
sessionId = sessionId.substring(0,
sessionId.length() - this.jvmRoute.length());
}
複製程式碼
2、回寫Cookie
中的程式碼片段
if (this.jvmRoute != null) {
actualCookieValue = requestedCookieValue + this.jvmRoute;
}
複製程式碼
jvm_route
是Nginx
中的一個模組,其作用是通過session cookie
的方式來獲取session
粘性。如果在cookie
和url
中並沒有session
,則這只是個簡單的 round-robin
負載均衡。其具體過程分為以下幾步:
- 1.第一個請求過來,沒有帶
session
資訊,jvm_route
就根據round robin
策略發到一臺tomcat
上面。 - 2.
tomcat
新增上session
資訊,並返回給客戶。 - 3.使用者再次請求,
jvm_route
看到session
中有後端伺服器的名稱,它就把請求轉到對應的伺服器上。
從本質上來說,jvm_route
也是解決session
共享的一種解決方式。這種和 SpringSession系列-分散式Session實現方案 中提到的基於IP-HASH
的方式有點類似。那麼同樣,這裡存在的問題是無法解決當機後session
資料轉移的問題,既當機就丟失。
DefaultCookieSerializer
中除了Cookie
的讀寫之後,還有一些細節也值得關注下,比如對Cookie
中值的驗證、remember-me
的實現等。