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

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> SpringSession系列-sessionId解析和Cookie读写策略

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

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

sessionId 解析策略

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

这两个类就分别对应 SpringSession解析 sessionId的两种不同的实现策略。再深入了解不同策略的实现细节之前,先来看下 HttpSessionIdResolver接口定义的一些行为有哪些。

HttpSessionIdResolver

HttpSessionIdResolver定义了 sessionId解析策略的契约( Contract)。允许通过请求解析sessionId,并通过响应发送sessionId或终止会话。接口定义如下:


public interface HttpSessionIdResolver {
    ListString 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`。
  • setSessionId:将给定的 sessionId发送给客户端。这个方法是在创建一个新 session时被调用,并告知客户端新 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 ListString 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的处理
  • DefaultCookieSerializer回写 Cookie的的具体实现,读 Cookie在 SpringSession系列-请求与响应重写 这篇文章中有介绍过,这里不再赘述。

    CookieValue

    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
    

    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`中有后端服务器的名称,它就把请求转到对应的服务器上。
  • 2. tomcat添加上 session 信息,并返回给客户。

    从本质上来说, jvm_route也是解决 session共享的一种解决方式。这种和 SpringSession系列-分布式Session实现方案 中提到的基于 IP-HASH的方式有点类似。那么同样,这里存在的问题是无法解决宕机后 session数据转移的问题,既宕机就丢失。

    DefaultCookieSerializer 中除了 Cookie的读写之后,还有一些细节也值得关注下,比如对 Cookie中值的验证、 remember-me的实现等。

    参考

  • SpringSession官方文档
  • jvm_router原理
  • SpringSession中文注释持续更新代码分支

  • jvm_router原理

    由于微信公共号中一些连接会失效,所以如有需要,可以点击下方阅读全文

    原文始发于微信公众号(glmapper工作室):

    本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

    本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

    原文链接:blog.ouyangsihai.cn >> SpringSession系列-sessionId解析和Cookie读写策略


     上一篇
    SpringSession——集成SpringBoot SpringSession——集成SpringBoot
    springSession是 spring 旗下的一个项目,把 servlet 容器实现的 httpSession替换为 springSession,专注于解决 session管理问题。可简单快速且无缝的集成到我们的应用中。本文
    下一篇 
    SpringBoot中的事件机制 SpringBoot中的事件机制
    微信公众号:**glmapper工作室** 掘金专栏:glmapper 微          博:疯狂的石头_henu 欢迎关注,一起学习分享技术 在这篇文章中聊一聊 Spring 中的扩展机制(一)中对 Spring中的事件机制进