SpringSession的源码解析(生成session,保存session,写入cookie全流程分析)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
云解析 DNS,旗舰版 1个月
简介: 上一篇文章主要介绍了如何使用SpringSession,其实SpringSession的使用并不是很难,无非就是引入依赖,加下配置。但是,这仅仅只是知其然,要知其所以然,我们还是需要深入源码去理解。

前言

上一篇文章主要介绍了如何使用SpringSession,其实SpringSession的使用并不是很难,无非就是引入依赖,加下配置。但是,这仅仅只是知其然,要知其所以然,我们还是需要深入源码去理解。在看本文先我们先想想,下面这些问题Session是啥时候创建的呢?通过什么来创建的呢?创建之后如何保存到Redis?又是如何把SessionId设置到Cookie中的呢?带着这一系列的问题,今天就让我们来揭开SpringSession的神秘面纱,如果读者朋友们看完本文之后能够轻松的回答上面的问题,那本文的作用也就达到了。当然,如果您已经对这些知识了若指掌,那么就不需要看本文了。

看源码的过程真的是一个很枯燥乏味的过程,但是弄清楚了其调用过程之后,又是很让人兴奋的,话不多说,直接进入正题。

基础介绍

默认参数的设置

首先,我们从添加的SpringSession的配置类来看起,如下,是一段很基础的配置代码,就添加了@Configuration注解和@EnableRedisHttpSession注解。其中@Configuration注解标注在类上,相当于把该类作为spring的xml配置文件中的<beans>,作用为:配置spring容器(应用上下文),@EnableRedisHttpSession注解的作用是使SpringSession生效。

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1)
public class SessionConfig {

点到EnableRedisHttpSession注解中,我们可以看到里面定义了RedisHttpSessionConfiguration的设置类,以及一些基础参数的设置,例如:session默认的失效时间,存入到redis的key的前缀名,这些我们参数我们在使用注解时都可以重新设置。例如:maxInactiveIntervalInSeconds设置为-1表示用不失效。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(RedisHttpSessionConfiguration.class)
@Configuration
public @interface EnableRedisHttpSession {
    //默认最大的失效时间是30分钟
    int maxInactiveIntervalInSeconds() default MapSession.DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS(1800秒);
  public static final int DEFAULT_MAX_INACTIVE_INTERVAL_SECONDS = 1800;
    //存入到redis的key的前缀名
    public static final String DEFAULT_NAMESPACE = "spring:session";
    String redisNamespace() default RedisOperationsSessionRepository.DEFAULT_NAMESPACE;
}

RedisHttpSessionConfiguration类是一个设置类,内部的作用主要是实例化RedisOperationsSessionRepository对象和RedisMessageListenerContainer对象等以及设置操作redis的工具类。


主要类的说明

类名 作用
RedisHttpSessionConfiguration 定义RedisOperationsSessionRepository等类的对象
SessionRepositoryFilter 过滤器,操作session的入口类
SessionRepositoryRequestWrapper 是SessionRepositoryFilter内部类,包装HttpRequest请求,调用RedisOperationsSessionRepository类相关的方法都是通过其完成
CookieHttpSessionIdResolver 这个类主要是调用DefaultCookieSerializer类的方法将sessionid存入cookie中,或者从cookie中读取sessionid,并返回给他的上一层
DefaultCookieSerializer 这个类是真正的操作cookie的类,设置cookie的相关属性,只需要重新实例化这个类即可
RedisOperationsSessionRepository 这个类的作用是生成session,并将session保存到redis中,另外就是根据sessionid查找session
RedisSession 这个类就是Spring Session的真正的实例对象,这是原始的session

操作session(生成session,保存session等过程)的时序图

首先,我们先看一下生成Session的调用时序图。

1. 调用的入口还是SessionRepositoryFilter类(PS:Spring是通过责任链的模式来执行每个过滤器的)的doFilterInternal方法。

@Override
  protected void doFilterInternal(HttpServletRequest request,
    HttpServletResponse response, FilterChain filterChain)
    throws ServletException, IOException {
  //省略部分代码
    SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(
    request, response, this.servletContext);
  try {
    //执行其他过滤器
    filterChain.doFilter(wrappedRequest, wrappedResponse);
  }
  finally {
  //wrappedRequest是SessionRepositoryRequestWrapper类的一个实例
    wrappedRequest.commitSession();
  }
  }

2. SessionRepositoryRequestWrapper类的getSession(true)方法

经过断点调试,并查看调用栈,发现调用这个filterChain.doFilter(wrappedRequest, wrappedResponse);方法之后,最终会调用到SessionRepositoryRequestWrapper类的getSession(true)方法。其中,SessionRepositoryRequestWrapper类是SessionRepositoryFilter类的一个私有的不可被继承,被重写的内部类。

public HttpSessionWrapper getSession(boolean create) {
    //1. 获取HttpSessionWrapper实例,如果可以获取到,则说明session已经生成了。就直接返回
    HttpSessionWrapper currentSession = getCurrentSession();
    if (currentSession != null) {
    return currentSession;
    }
    //如果可以获取到session
    S requestedSession = getRequestedSession();
    //如果HttpSessionWrapper实例为空,则需要将session对象封装到HttpSessionWrapper实例中,并设置到HttpRequestSerlvet中
    if (requestedSession != null) {
    if (getAttribute(INVALID_SESSION_ID_ATTR) == null) {
      requestedSession.setLastAccessedTime(Instant.now());
      this.requestedSessionIdValid = true;
      currentSession = new HttpSessionWrapper(requestedSession, getServletContext());
      currentSession.setNew(false);
      setCurrentSession(currentSession);
      return currentSession;
    }
    }
    //如果获取不到session,则进入下面分支,创建session
    else {
    //省略部分代码
    //如果create为false,直接返回null
    if (!create) {
    return null;
    }
      //省略部分代码
    //如果create为true,则调用RedisOperationsSessionRepository类的createSession方法创建session实例
    S session = SessionRepositoryFilter.this.sessionRepository.createSession();
    session.setLastAccessedTime(Instant.now());
    currentSession = new HttpSessionWrapper(session, getServletContext());
    setCurrentSession(currentSession);
    return currentSession;
  }

如上代码所示:getSession(boolean create) 方法主要有两块,1. 获取session实例,如果请求头中带着sessionid,则表示不是第一次请求,是可以获取到session的。2. 如果浏览器是第一次请求应用(没有sessionid)则获取不到session实例,需要创建session实例。在拿到生成的Session对象之后,紧接着会创建一个HttpSessionWrapper实例,并将前面生成的session传入其中,方便后面取用,然后将HttpSessionWrapper实例放入当前请求会话HttpServletRequest中,(Key是.CURRENT_SESSION,value是HttpSessionWrapper的实例)。

3. RedisOperationsSessionRepository类的createSession()方法

从前面的代码分析我们可以知道如果获取不到session实例,则会调用createSession()方法进行创建。这个方法是在RedisOperationsSessionRepository类中,该方法比较简单,主要就是实例化RedisSession对象。其中RedisSession对象中包括了sessionid,creationTime,maxInactiveInterval和lastAccessedTime等属性。其中原始的sessionid是一段唯一的UUID字符串。

@Override
  public RedisSession createSession() {
  //实例化RedisSession对象
  RedisSession redisSession = new RedisSession();
  if (this.defaultMaxInactiveInterval != null) {
    //设置session的失效时间
    redisSession.setMaxInactiveInterval(
      Duration.ofSeconds(this.defaultMaxInactiveInterval));
  }
  return redisSession;
  }
  RedisSession() {
    this(new MapSession());
    this.delta.put(CREATION_TIME_KEY, getCreationTime().toEpochMilli());
    this.delta.put(MAX_INACTIVE_INTERVAL_KEY,
      (int) getMaxInactiveInterval().getSeconds());
    this.delta.put(LAST_ACCESSED_TIME_KEY, getLastAccessedTime().toEpochMilli());
    this.isNew = true;
    this.flushImmediateIfNecessary();
  }

另外,doFilterInternal方法在调用完其他方法之后,在finally代码块中会调用SessionRepositoryRequestWrapper类内部的commitSession()方法,而commitSession()方法会保存session信息到Redis中,并将sessionid写到cookie中。我们接着来看看commitSession()方法。

private void commitSession() {
    //当前请求会话中获取HttpSessionWrapper对象的实例
    HttpSessionWrapper wrappedSession = getCurrentSession();
    //如果wrappedSession为空则调用expireSession写入一个空值的cookie
    if (wrappedSession == null) {
    if (isInvalidateClientSession()) {
      SessionRepositoryFilter.this.httpSessionIdResolver.expireSession(this,
        this.response);
    }
    }
    else {
    //获取session
    S session = wrappedSession.getSession();
    clearRequestedSessionCache();
    SessionRepositoryFilter.this.sessionRepository.save(session);
    String sessionId = session.getId();
    if (!isRequestedSessionIdValid()
      || !sessionId.equals(getRequestedSessionId())) {
      SessionRepositoryFilter.this.httpSessionIdResolver.setSessionId(this,
        this.response, sessionId);
    }
    }
  }

第一步就是从当前请求会话中获取HttpSessionWrapper对象的实例,如果实例获取不到则向Cookie中写入一个空值。如果可以获取到实例的话,则从实例中获取Session对象。获取到Session对象之后则调用RedisOperationsSessionRepository类的save(session)方法将session信息保存到Redis中,其中redis的名称前缀是spring:session。将数据保存到Redis之后

紧接着获取sessionid,最后调用CookieHttpSessionIdResolver类的setSessionId方法将sessionid设置到Cookie中。

4. CookieHttpSessionIdResolver类的setSessionId方法

@Override
  public void setSessionId(HttpServletRequest request, HttpServletResponse response,
    String sessionId) {
    //如果sessionid等于请求头中的sessionid,则直接返回
  if (sessionId.equals(request.getAttribute(WRITTEN_SESSION_ID_ATTR))) {
    return;
  }
  //将sessionid设置到请求头中
  request.setAttribute(WRITTEN_SESSION_ID_ATTR, sessionId);
  //将sessionid写入cookie中
  this.cookieSerializer
    .writeCookieValue(new CookieValue(request, response, sessionId));
  }

从上代码我们可以看出,setSessionId方法主要就是将生成的sessionid设置到请求会话中,然后调用DefaultCookieSerializer类的writeCookieValue方法将sessionid设置到cookie中。

5. DefaultCookieSerializer类的writeCookieValue方法

@Override
  public void writeCookieValue(CookieValue cookieValue) {
  HttpServletRequest request = cookieValue.getRequest();
  HttpServletResponse response = cookieValue.getResponse();
  StringBuilder sb = new StringBuilder();
  //设置cookie的名称,默认是SESSION
  sb.append(this.cookieName).append('=');
  //设置cookie的值,就是传入的sessionid
  String value = getValue(cookieValue);
  if (value != null && value.length() > 0) {
    validateValue(value);
    sb.append(value);
  }
  //设置cookie的失效时间
  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);
  //设置Domain属性,默认就是当前请求的域名,或者ip
  if (domain != null && domain.length() > 0) {
    validateDomain(domain);
    sb.append("; Domain=").append(domain);
  }
  //设置Path属性,默认是当前项目名(例如:/spring-boot-session),可重设
  String path = getCookiePath(request);
  if (path != null && path.length() > 0) {
    validatePath(path);
    sb.append("; Path=").append(path);
  }
  if (isSecureCookie(request)) {
    sb.append("; Secure");
  }
  //设置在HttpOnly是否只读属性。
  if (this.useHttpOnlyCookie) {
    sb.append("; HttpOnly");
  }
  if (this.sameSite != null) {
    sb.append("; SameSite=").append(this.sameSite);
  }
  //将设置好的cookie放入响应头中
  response.addHeader("Set-Cookie", sb.toString());
  }

分析到这儿整个session生成的过程,保存到session的过程,写入到cookie的过程就分析完了。如果下次遇到session共享的问题我们处理起来也就得心应手了。

例如:如果要实现同域名下不同项目的项目之间session共享,我们只需要改变Path属性即可。

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = -1)
public class SessionConfig {
    @Bean
    public DefaultCookieSerializer defaultCookieSerializer() {
        DefaultCookieSerializer defaultCookieSerializer = new DefaultCookieSerializer();
        defaultCookieSerializer.setCookiePath("/");
        return defaultCookieSerializer;
    }
}

如果要指定域名的话,我们只需要设置DomainName属性即可。其他的也是同理,在此就不在赘述了。

总结

本文按照代码运行的顺序,一步步分析了session的创建,保存到redis,将sessionid交由cookie托管的过程。分析完源码之后,我们知道了session的创建和保存到redis主要是由RedisOperationsSessionRepository类来完成。将sessionid交由cookie托管主要是由DefaultCookieSerializer类来完成,下一篇我们将介绍读取session的过程。

源代码

https://github.com/XWxiaowei/spring-boot-session-demo

相关文章
|
27天前
|
数据采集 自然语言处理 搜索推荐
基于qwen2.5的长文本解析、数据预测与趋势分析、代码生成能力赋能esg报告分析
Qwen2.5是一款强大的生成式预训练语言模型,擅长自然语言理解和生成,支持长文本解析、数据预测、代码生成等复杂任务。Qwen-Long作为其变体,专为长上下文场景优化,适用于大型文档处理、知识图谱构建等。Qwen2.5在ESG报告解析、多Agent协作、数学模型生成等方面表现出色,提供灵活且高效的解决方案。
130 49
|
13天前
|
PyTorch Shell API
Ascend Extension for PyTorch的源码解析
本文介绍了Ascend对PyTorch代码的适配过程,包括源码下载、编译步骤及常见问题,详细解析了torch-npu编译后的文件结构和三种实现昇腾NPU算子调用的方式:通过torch的register方式、定义算子方式和API重定向映射方式。这对于开发者理解和使用Ascend平台上的PyTorch具有重要指导意义。
|
17天前
|
缓存 监控 Java
Java线程池提交任务流程底层源码与源码解析
【11月更文挑战第30天】嘿,各位技术爱好者们,今天咱们来聊聊Java线程池提交任务的底层源码与源码解析。作为一个资深的Java开发者,我相信你一定对线程池并不陌生。线程池作为并发编程中的一大利器,其重要性不言而喻。今天,我将以对话的方式,带你一步步深入线程池的奥秘,从概述到功能点,再到背景和业务点,最后到底层原理和示例,让你对线程池有一个全新的认识。
47 12
|
18天前
|
测试技术 开发者 Python
使用Python解析和分析源代码
本文介绍了如何使用Python的`ast`模块解析和分析Python源代码,包括安装准备、解析源代码、分析抽象语法树(AST)等步骤,展示了通过自定义`NodeVisitor`类遍历AST并提取信息的方法,为代码质量提升和自动化工具开发提供基础。
32 8
|
16天前
|
调度 开发者
核心概念解析:进程与线程的对比分析
在操作系统和计算机编程领域,进程和线程是两个基本而核心的概念。它们是程序执行和资源管理的基础,但它们之间存在显著的差异。本文将深入探讨进程与线程的区别,并分析它们在现代软件开发中的应用和重要性。
34 4
|
1月前
|
存储 安全 搜索推荐
理解Session和Cookie:Java Web开发中的用户状态管理
理解Session和Cookie:Java Web开发中的用户状态管理
61 4
|
1月前
|
存储 缓存 网络协议
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点,GET、POST的区别,Cookie与Session
计算机网络常见面试题(二):浏览器中输入URL返回页面过程、HTTP协议特点、状态码、报文格式,GET、POST的区别,DNS的解析过程、数字证书、Cookie与Session,对称加密和非对称加密
|
2月前
|
缓存 Java Spring
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
文章比较了在Servlet和Spring Boot中获取Cookie、Session和Header的方法,并提供了相应的代码实例,展示了两种方式在实际应用中的异同。
190 3
servlet和SpringBoot两种方式分别获取Cookie和Session方式比较(带源码) —— 图文并茂 两种方式获取Header
|
2月前
|
存储 安全 数据安全/隐私保护
Cookie 和 Session 的区别及使用 Session 进行身份验证的方法
【10月更文挑战第12天】总之,Cookie 和 Session 各有特点,在不同的场景中发挥着不同的作用。使用 Session 进行身份验证是常见的做法,通过合理的设计和管理,可以确保用户身份的安全和可靠验证。
26 1
|
3月前
|
存储 缓存 数据处理
php学习笔记-php会话控制,cookie,session的使用,cookie自动登录和session 图书上传信息添加和修改例子-day07
本文介绍了PHP会话控制及Web常用的预定义变量,包括`$_REQUEST`、`$_SERVER`、`$_COOKIE`和`$_SESSION`的用法和示例。涵盖了cookie的创建、使用、删除以及session的工作原理和使用,并通过图书上传的例子演示了session在实际应用中的使用。
php学习笔记-php会话控制,cookie,session的使用,cookie自动登录和session 图书上传信息添加和修改例子-day07

推荐镜像

更多