SpringSession系列-sessionId解析和Cookie讀寫策略

glmapper發表於2018-12-22

首先需求在這裡說明下,SpringSession的版本迭代的過程中肯定會伴隨著一些類的移除和一些類的加入,目前本系列使用的版本是github上物件的master的程式碼流版本。如果有同學對其他版本中的一些類或者處理有疑惑,歡迎交流。

本篇將來介紹下SpringSession中兩種sessionId解析的策略,這個在之前的文章中其實是有提到過的,這裡再拿出來和SpringSessionCookie相關策略一起學習 下。

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:解析與當前請求相關聯的sessionIdsessionId可能來自Cookie或請求頭。
  • setSessionId:將給定的sessionId傳送給客戶端。這個方法是在建立一個新session時被呼叫,並告知客戶端新sessionId是什麼。
  • expireSession:指示客戶端結束當前session。當session無效時呼叫此方法,並應通知客戶端sessionId不再有效。比如,它可能刪除一個包含sessionIdCookie,或者設定一個HTTP響應頭,其值為空就表示客戶端不再提交sessionId

下面就針對上面提到的兩種策略來進行詳細的分析。

基於Cookie解析sessionId

這種策略對應的實現類是CookieHttpSessionIdResolver,通過從Cookie中獲取session;具體來說,這個實現將允許使用CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)指定Cookie序列化策略。預設的Cookie名稱是“SESSION”。建立一個session時,HTTP響應中將會攜帶一個指定 Cookie namevaluesessionIdCookieCookie 將被標記為一個 session cookieCookiedomain 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來完成的。CookieSerializerSpringSession中對於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的的具體實現,讀CookieSpringSession系列-請求與響應重寫 這篇文章中有介紹過,這裡不再贅述。
  • jvm_router的處理

CookieValue

CookieValueCookieSerializer中的內部類,封裝了向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_routeNginx中的一個模組,其作用是通過session cookie的方式來獲取session粘性。如果在cookieurl中並沒有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的實現等。

參考

相關文章