转载

SpringSession系列-sessionId解析和Cookie读写策略

首先需求在这里说明下,SpringSession的版本迭代的过程中肯定会伴随着一些类的移除和一些类的加入,目前本系列使用的版本是github上对象的master的代码流版本。如果有同学对其他版本中的一些类或者处理有疑惑,欢迎交流。

本篇将来介绍下 SpringSession 中两种 sessionId 解析的策略,这个在之前的文章中其实是有提到过的,这里再拿出来和 SpringSessionCookie 相关策略一起学习 下。

sessionId 解析策略

SpringSession 中对于 sessionId 的解析相关的策略是通过 HttpSessionIdResolver 这个接口来体现的。 HttpSessionIdResolver 有两个实现类:

SpringSession系列-sessionId解析和Cookie读写策略

这两个类就分别对应 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 的的具体实现,读 Cookie 在 SpringSession系列-请求与响应重写 这篇文章中有介绍过,这里不再赘述。
  • 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 的实现等。

原文  https://juejin.im/post/5c1e71176fb9a04a0b222929
正文到此结束
Loading...