首先需求在这里说明下,SpringSession的版本迭代的过程中肯定会伴随着一些类的移除和一些类的加入,目前本系列使用的版本是github上对象的master的代码流版本。如果有同学对其他版本中的一些类或者处理有疑惑,欢迎交流。
本篇将来介绍下 SpringSession
中两种 sessionId
解析的策略,这个在之前的文章中其实是有提到过的,这里再拿出来和 SpringSession
中 Cookie
相关策略一起学习 下。
SpringSession
中对于 sessionId
的解析相关的策略是通过 HttpSessionIdResolver
这个接口来体现的。 HttpSessionIdResolver
有两个实现类:
这两个类就分别对应 SpringSession
解析 sessionId
的两种不同的实现策略。再深入了解不同策略的实现细节之前,先来看下 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
。 下面就针对上面提到的两种策略来进行详细的分析。
这种策略对应的实现类是 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
操作提供的一种机制。下面细说。
这种策略对应的实现类是 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
解析 sessionId
的实现类 CookieHttpSessionIdResolver
中实际对于 Cookie
的读写操作都是通过 CookieSerializer
来完成的。 SpringSession
提供了 CookieSerializer
接口的默认实现 DefaultCookieSerializer
,当然在实际应用中,我们也可以自己实现这个接口,然后通过 CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)
方法来指定我们自己的实现方式。
PS:不得不说,强大的用户扩展能力真的是 Spring
家族的优良家风。
篇幅有限,这里就只看下两个点:
CookieValue
存在的意义是什么 DefaultCookieSerializer
回写 Cookie
的的具体实现,读 Cookie
在 SpringSession系列-请求与响应重写 这篇文章中有介绍过,这里不再赘述。 CookieValue
是 CookieSerializer
中的内部类,封装了向 HttpServletResponse
写入所需的所有信息。其实 CookieValue
的存在并没有什么特殊的意义,个人觉得作者一开始只是想通过 CookieValue
的封装来简化回写 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 复制代码
在 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
负载均衡。其具体过程分为以下几步:
session
信息, jvm_route
就根据 round robin
策略发到一台 tomcat
上面。 tomcat
添加上 session
信息,并返回给客户。 jvm_route
看到 session
中有后端服务器的名称,它就把请求转到对应的服务器上。 从本质上来说, jvm_route
也是解决 session
共享的一种解决方式。这种和 SpringSession系列-分布式Session实现方案 中提到的基于 IP-HASH
的方式有点类似。那么同样,这里存在的问题是无法解决宕机后 session
数据转移的问题,既宕机就丢失。
DefaultCookieSerializer
中除了 Cookie
的读写之后,还有一些细节也值得关注下,比如对 Cookie
中值的验证、 remember-me
的实现等。