• 欢迎访问 winrains 的个人网站!
  • 本网站主要从互联网整理和收集了与Java、网络安全、Linux等技术相关的文章,供学习和研究使用。如有侵权,请留言告知,谢谢!

SpringSession系列:请求与响应重写

Spring Session winrains 来源:GuoLei Song 6个月前 (03-21) 24次浏览
我们知道,HttpServletRequsetHttpServletResponseServlet标准所指定的Java语言与Web容器进行交互的接口。接口本身只规定java语言对web容器进行访问的行为方式,而具体的实现是由不同的web容器在其内部实现的。那么在运行期,当我们需要对HttpServletRequsetHttpServletResponse的默认实例进行扩展时,我们就可以继承HttpServletRequestWrapperHttpServletResponseWrapper来实现。

SpringSession中因为我们要实现不依赖容器本身的getSession 实现,因此需要扩展 HttpServletRequset,通过重写getSession来实现分布式session的能力。下面就来看下SpringSession中对于HttpServletRequset的扩展。

1、请求重写

SpringSession 中对于请求重写,在能力上主要体现在存储方面,也就是getSession方法上。在 SessionRepositoryFilter 这个类中,是通过内部类的方式实现了对HttpServletRequsetHttpServletResponse的扩展。

1.1 HttpServletRequset 扩展实现

private final class SessionRepositoryRequestWrapper
      extends HttpServletRequestWrapper {
  // HttpServletResponse 实例
  private final HttpServletResponse response;
  // ServletContext 实例
  private final ServletContext servletContext;
        // requestedSession session对象
        private S requestedSession; 
        // 是否缓存 session
        private boolean requestedSessionCached;
  // sessionId
  private String requestedSessionId;
  // sessionId 是否有效
  private Boolean requestedSessionIdValid;
  // sessionId 是否失效
  private boolean requestedSessionInvalidated;
  
  // 省略方法
}

1.2 构造方法

private SessionRepositoryRequestWrapper(HttpServletRequest request,
    HttpServletResponse response, ServletContext servletContext) {
  super(request);
  this.response = response;
  this.servletContext = servletContext;
}

构造方法里面将 HttpServletRequestHttpServletResponse 以及 ServletContext 实例传递进来,以便于后续扩展使用。

1.3 getSession 方法

@Override
public HttpSessionWrapper getSession(boolean create) {
    // 从当前请求线程中获取 session
  HttpSessionWrapper currentSession = getCurrentSession();
  // 如果有直接返回
  if (currentSession != null) {
    return currentSession;
  }
  // 从请求中获取 session,这里面会涉及到从缓存中拿session的过程
  S requestedSession = getRequestedSession();
  if (requestedSession != null) {
      // 无效的会话id(不支持的会话存储库)请求属性名称。
      // 这里看下当前的sessionId是否有效
    if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
        // 设置当前session的最后访问时间,用于延迟session的有效期
      requestedSession.setLastAccessedTime(Instant.now());
      // 将requestedSessionIdValid置为true
      this.requestedSessionIdValid = true;
      // 包装session
      currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
      // 不是新的session,如果是新的session则需要改变sessionId
      currentSession.setNew(false);
      // 将session设置到当前请求上下文
      setCurrentSession(currentSession);
      // 返回session
      return currentSession;
    }
  }
  else {
    // 这里处理的是无效的sessionId的情况,但是当前请求线程 session有效
    if (SESSION_LOGGER.isDebugEnabled()) {
      SESSION_LOGGER.debug(
          "No session found by id: Caching result for getSession(false) for this HttpServletRequest.");
    }
    // 将invalidSessionId置为true
    setAttribute(INVALID_SESSION_ID_ATTR, "true");
  }
  // 是否需要创建新的session
  if (!create) {
    return null;
  }
  if (SESSION_LOGGER.isDebugEnabled()) {
    SESSION_LOGGER.debug(
        "A new session was created. To help you troubleshoot where the session was created we provided a StackTrace (this is not an error). You can prevent this from appearing by disabling DEBUG logging for "
            + SESSION_LOGGER_NAME,
        new RuntimeException(
            "For debugging purposes only (not an error)"));
  }
  // 创建新的session
  S session = SessionRepositoryFilter.this.sessionRepository.createSession();
  // 设置最后访问时间,也就是指定了当前session的有效期限
  session.setLastAccessedTime(Instant.now());
  // 包装下当前session
  currentSession = new HttpSessionWrapper(session, getServletContext());
  //设置到当前请求线程
  setCurrentSession(currentSession);
  return currentSession;
}

上面这段代码有几个点,这里单独来解释下。

  • getCurrentSession
    • 这是为了在同一个请求过程中不需要重复的去从存储中获取session,在一个新的进来时,将当前的 session 设置到当前请求中,在后续处理过程如果需要getSession就不需要再去存储介质中再拿一次。
  • getRequestedSession
    • 这个是根据请求信息去取session,这里面就包括了sessionId解析,从存储获取session对象等过程。
  • 是否创建新的session对象
    • 在当前请求中和存储中都没有获取到session信息的情况下,这里会根据create参数来判断是否创建新的session。这里一般用户首次登录时或者session失效时会走到。

1.4 getRequestedSession

根据请求信息来获取session对象

private S getRequestedSession() {
    // 缓存的请求session是否存在
  if (!this.requestedSessionCached) {
            // 获取 sessionId
            List<String> sessionIds = SessionRepositoryFilter.this.httpSessionIdResolver
            		.resolveSessionIds(this);
            // 通过sessionId来从存储中获取session
            for (String sessionId : sessionIds) {
            	if (this.requestedSessionId == null) {
            		this.requestedSessionId = sessionId;
            	}
            	S session = SessionRepositoryFilter.this.sessionRepository
            			.findById(sessionId);
            	if (session != null) {
            		this.requestedSession = session;
            		this.requestedSessionId = sessionId;
            		break;
            	}
            }
            this.requestedSessionCached = true;
  }
  return this.requestedSession;
}

这段代码还是很有意思的,这里获取sessionId返回的是个列表。当然这里是SpringSession的实现策略,因为支持session,所以这里以列表的形式返回的。OK,继续来看如何解析sessionId的:

http://image.winrains.cn/2020/03/20200320134403-deb96.png

这里可以看到SpringSession对于sessionId获取的两种策略,一种是基于cookie,一种是基于header;分别来看下具体实现。

1.4.1 CookieHttpSessionIdResolver 获取 sessionId

CookieHttpSessionIdResolver 中获取sessionId的核心代码如下:

http://image.winrains.cn/2020/03/20200320134404-4ac6e.png
其实这里没啥好说的,就是读cookie。从requestcookie信息拿出来,然后遍历找当前sessionId对应的cookie,这里的判断也很简单, 如果是以SESSION开头,则表示是 SessionId,毕竟cookie是共享的,不只有sessionId,还有可能存储其他内容。

另外这里面有个 jvmRoute,这个东西实际上很少能够用到,因为大多数情况下这个值都是null。这个我们在分析CookieSerializer时再来解释。

1.4.2 HeaderHttpSessionIdResolver 获取 sessionId

http://image.winrains.cn/2020/03/20200320134405-544a3.png
这个获取更直接粗暴,就是根据 headerNameheader 中取值。

回到getRequestedSession,剩下的代码中核心的都是和sessionRepository这个有关系,这部分就会涉及到存储部分。不在本篇的分析范围之内,会在存储实现部分来分析。

1.5 HttpSessionWrapper

http://image.winrains.cn/2020/03/20200320134406-1c99b.png

上面的代码中当我们拿到session实例是通常会包装下,那么用到的就是这个HttpSessionWrapper

HttpSessionWrapper 继承了 HttpSessionAdapter,这个HttpSessionAdapter就是将SpringSession 转换成一个标准HttpSession的适配类。HttpSessionAdapter 实现了标准servlet规范的HttpSession接口。

1.5.1 HttpSessionWrapper

HttpSessionWrapper 重写了 invalidate方法。从代码来看,调用该方法产生的影响是:

  • requestedSessionInvalidated 置为true,标识当前 session 失效。
  • 将当前请求中的session设置为null,那么在请求的后续调用中通过getCurrentSession将拿不到session信息。
  • 当前缓存的 session 清楚,包括sessionId,session实例等。
  • 删除存储介质中的session对象。

1.5.2 HttpSessionAdapter

SpringSession和标准HttpSession的配置器类。这个怎么理解呢,来看下一段代码:

@Override
public Object getAttribute(String name) {
  checkState();
  return this.session.getAttribute(name);
}

对于基于容器本身实现的HttpSession来说,getAttribute的实现也是有容器本身决定。但是这里做了转换之后,getAttribute将会通过SpringSession中实现的方案来获取。其他的API适配也是基于此实现。

SessionCommittingRequestDispatcher

实现了 RequestDispatcher 接口。关于RequestDispatcher可以参考这篇文章【Servlet】关于RequestDispatcher的原理SessionCommittingRequestDispatcherforward的行为并没有改变。
对于include则是在include之前提交session。为什么这么做呢?

因为include方法使原先的Servlet和转发到的Servlet都可以输出响应信息,即原先的Servlet还可以继续输出响应信息;即请求转发后,原先的Servlet还可以继续输出响应信息,转发到的Servlet对请求做出的响应将并入原先Servlet的响应对象中。

所以这个在include调用之前调用commit,这样可以确保被包含的Servlet程序不能改变响应消息的状态码和响应头。

2 响应重写

响应重写的目的是确保在请求提交时能够把session保存起来。来看下SessionRepositoryResponseWrapper类的实现:

http://image.winrains.cn/2020/03/20200320134407-8127e.png
这里面实现还就是重写onResponseCommitted,也就是上面说的,在请求提交时能够通过这个回调函数将session保存到存储容器中。

2.1 session 提交

最后来看下 commitSession

http://image.winrains.cn/2020/03/20200320134408-7d3cd.png

这个过程不会再去存储容器中拿session信息,而是直接从当前请求中拿。如果拿不到,则在回写cookie时会将当前session对应的cookie值设置为空,这样下次请求过来时携带的sessionCookie就是空,这样就会重新触发登陆。

如果拿到,则清空当前请求中的session信息,然后将session保存到存储容器中,并且将sessionId回写到cookie中。

小结

本篇主要对SpringSession中重写RequestResponse进行了分析。通过重写Request请求来将session的存储与存储容器关联起来,通过重写Response来处理session提交,将session保存到存储容器中。

后面我们会继续来分析SpringSession的源码。最近也在学习链路跟踪相关的技术,也准备写一写,有兴趣的同学可以一起讨论。

作者:GuoLei Song

来源:http://www.glmapper.com/2018/11/24/spring-session-req-resp/


版权声明:文末如注明作者和来源,则表示本文系转载,版权为原作者所有 | 本文如有侵权,请及时联系,承诺在收到消息后第一时间删除 | 如转载本文,请注明原文链接。
喜欢 (1)