当前位置: 首页 > news >正文

SpringSession原理简析

本文借鉴于:Spring-Session 原理简析 - 知乎 (zhihu.com)

目录

概述

使用方式

原理

总结


概述

Session的原理

Session是存在服务器的一种用来存放用户数据的类哈希表结构,当浏览器第一次发送请求的时候服务器会生成一个hashtable和一个sessionid,sessionid来唯一标识这个hashtable,响应的时候会通过一个响应头set-cookie返回给浏览器,浏览器再将这个sessionid存储在一个名为JESSIONID的cookie中。 接着浏览器在发送第二次请求时,就会带上这个cookie,这个cookie会存储sessionid,并发送给服务器,服务器根据这个sessionid找到对应的用户信息。

分布式下Session共享的问题

如果在不同域名环境下需要进行session共享,比如在auth.gulimall.com登录成功之后要将登陆成功的用户信息共享给gulimall.com服务,由于域名不同,普通的session就会不起作用。并且如果是同一个服务,复制多份,session也不会共享。

SpringSession

SpringSession 是一个 Spring 项目中用于管理和跟踪会话的框架。它提供了一种抽象层,使得会话数据可以存储在不同的后端数据结构中(例如内存、数据库、Redis 等),并且支持跨多个请求的会话管理。

Spring Session 的核心功能包括:

(1)跨多个请求共享会话数据:SpringSession 使用一个唯一的会话标识符来跟踪用户的会话,并且可以在不同的请求中共享会话数据,无论是在同一个应用程序的不同服务器节点之间,还是在不同的应用程序之间。

(2)多种后端存储支持:SpringSession 支持多种后端存储,包括内存、数据库和缓存系统(如 Redis)。你可以根据应用程序的需求选择合适的存储方式。

(3)会话过期管理:SpringSession 提供了会话过期管理的功能,可以根据一定的策略自动清理过期的会话数据。 会话事件监听:Spring Session 允许开发人员注册会话事件监听器,以便在会话创建、销毁或属性更改时执行一些自定义逻辑。

(4)使用 SpringSession 可以使得在分布式环境中管理会话变得更加简单和灵活,同时也提供了更多的扩展性和可定制性。

SpringSession 可以在多个微服务之间共享 session 数据。

使用方式

添加依赖

<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency><groupId>org.springframework.session</groupId><artifactId>spring-session-data-redis</artifactId>
</dependency>

添加注解@EnableRedisHttpSession

@Configuration
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 86400*30)
public class RedisSessionConfig {}

maxInactiveIntervalInSeconds: 设置 Session 失效时间,使用 Redis Session 之后,原 Spring Boot 的 server.session.timeout 属性不再生效。也可以在yml文件上配置

spring:  session:store-type: redistimeout: 30m #这个过期时间是在浏览器关闭之后才开始计时

经过上面的配置后,Session 调用就会自动去Redis存取。另外,想要达到 Session 共享的目的,只需要在其他的系统上做同样的配置即可。

原理

看了上面的配置,我们知道开启 Redis Session 的“秘密”在 @EnableRedisHttpSession 这个注解上。打开 @EnableRedisHttpSession 的源码(shift+shift):

/*** 用于启用基于Redis的HTTP会话管理的配置注解。* 该注解会将RedisHttpSessionConfiguration配置类导入到Spring配置中,* 从而支持将会话信息存储在Redis中。** @author <NAME>* @see RedisHttpSessionConfiguration*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({RedisHttpSessionConfiguration.class})
@Configuration(proxyBeanMethods = false
)
public @interface EnableRedisHttpSession {/*** 会话的最大不活动间隔时间(秒)。默认值为1800秒(30分钟)。* 该属性定义了会话在Redis中存储的有效期。* * @return 最大不活动间隔时间(秒)*/int maxInactiveIntervalInSeconds() default 1800;/*** Redis中用于存储会话数据的命名空间。默认值为"spring:session"。* 通过设置不同的命名空间,可以实现多个应用会话的隔离。* * @return Redis存储会话数据的命名空间*/String redisNamespace() default "spring:session";/*** 已弃用的Redis刷新模式设置。建议使用{@link #flushMode()}替代。* * @return Redis刷新模式,默认为ON_SAVE* @deprecated 使用{@link #flushMode()}替代*/@DeprecatedRedisFlushMode redisFlushMode() default RedisFlushMode.ON_SAVE;/*** 定义Redis中数据的刷新模式。可配置为在每次保存属性时刷新(ON_SAVE),* 或者在请求结束时刷新(ON_COMMIT)。* * @return 刷新模式,默认为ON_SAVE*/FlushMode flushMode() default FlushMode.ON_SAVE;/*** 定义清理过期会话的CRON表达式。默认值为"0 * * * * *",即每分钟执行一次。* 通过调整该表达式,可以控制清理过期会话的频率。* * @return 清理过期会话的CRON表达式*/String cleanupCron() default "0 * * * * *";/*** 定义会话属性保存的模式。可配置为仅在设置属性时保存(ON_SET_ATTRIBUTE),* 或者在每次请求结束时保存(ALWAYS)。* * @return 会话属性保存模式,默认为ON_SET_ATTRIBUTE*/SaveMode saveMode() default SaveMode.ON_SET_ATTRIBUTE;
}

这个注解的主要作用是注册一个 SessionRepositoryFilter,这个 Filter 会拦截所有的请求,对 Session 进行操作。

SessionRepositoryFilter 拦截到请求后,会先将 request 和 response 对象转换成 Spring 内部的包装类 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 对象。SessionRepositoryRequestWrapper 类重写了原生的getSession方法。代码如下:

@Override
public HttpSessionWrapper getSession(boolean create) {//通过request的getAttribue方法查找CURRENT_SESSION属性,有直接返回HttpSessionWrapper currentSession = getCurrentSession();if (currentSession != null) {return currentSession;}//查找客户端中一个叫SESSION的cookie,通过sessionRepository对象根据SESSIONID去Redis中查找SessionS requestedSession = getRequestedSession();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);//将Session设置到request属性中setCurrentSession(currentSession);//返回Sessionreturn currentSession;}}else {// This is an invalid session id. No need to ask again if// request.getSession is invoked for the duration of this requestif (SESSION_LOGGER.isDebugEnabled()) {SESSION_LOGGER.debug("No session found by id: Caching result for getSession(false) for this HttpServletRequest.");}setAttribute(INVALID_SESSION_ID_ATTR, "true");}//不创建Session就直接返回nullif (!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)"));}//通过sessionRepository创建RedisSession这个对象,可以看下这个类的源代码,如果//@EnableRedisHttpSession这个注解中的redisFlushMode模式配置为IMMEDIATE模式,会立即//将创建的RedisSession同步到Redis中去。默认是不会立即同步的。S session = SessionRepositoryFilter.this.sessionRepository.createSession();session.setLastAccessedTime(Instant.now());currentSession = new HttpSessionWrapper(session, getServletContext());setCurrentSession(currentSession);return currentSession;
}

当调用 SessionRepositoryRequestWrapper 对象的getSession方法拿 Session 的时候,会先从当前请求的属性中查找CURRENT_SESSION属性,如果能拿到直接返回,这样操作能减少Redis操作,提升性能。

到现在为止我们发现如果redisFlushMode配置为 ON_SAVE 模式的话,Session 信息还没被保存到 Redis 中,那么这个同步操作到底是在哪里执行的呢?

仔细看代码,我们发现 SessionRepositoryFilter 的doFilterInternal方法最后有一个 finally 代码块,这个代码块的功能就是将 Session同步到 Redis。

@Override
protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {request.setAttribute(SESSION_REPOSITORY_ATTR, this.sessionRepository);SessionRepositoryRequestWrapper wrappedRequest = new SessionRepositoryRequestWrapper(request, response, this.servletContext);SessionRepositoryResponseWrapper wrappedResponse = new SessionRepositoryResponseWrapper(wrappedRequest, response);try {filterChain.doFilter(wrappedRequest, wrappedResponse);}finally {//将Session同步到Redis,同时这个方法还会将当前的SESSIONID写到cookie中去,同时还会发布一//SESSION创建事件到队列里面去wrappedRequest.commitSession();}
}

总结

主要的核心类有:

  • EnableRedisHttpSession:开启 Session 共享功能;
  • RedisHttpSessionConfiguration:配置类,一般不需要我们自己配置,主要功能是配置 SessionRepositoryFilter 和 RedisOperationsSessionRepository 这两个Bean;
  • SessionRepositoryFilter:拦截器,Spring-Session 框架的核心;
  • RedisOperationsSessionRepository:可以认为是一个 Redis 操作的客户端,有在 Redis 中进行增删改查 Session 的功能;
  • SessionRepositoryRequestWrapper:Request 的包装类,主要是重写了getSession方法
  • SessionRepositoryResponseWrapper:Response的包装类。

原理简要总结:

当请求进来的时候,SessionRepositoryFilter 会先拦截到请求,将 request 和 response 对象转换成 SessionRepositoryRequestWrapper 和 SessionRepositoryResponseWrapper 。后续当第一次调用 request 的getSession方法时,会调用到 SessionRepositoryRequestWrapper 的getSession方法。这个方法是被重写过的,逻辑是先从 request 的属性中查找,如果找不到,再查找一个key值是"SESSION"的 Cookie,通过这个 Cookie 拿到 SessionId 去 Redis 中查找,如果查不到,就直接创建一个RedisSession 对象,同步到 Redis 中。

说的简单点就是:拦截请求,将之前在服务器内存中进行 Session 创建、销毁的动作,改成在 Redis 中进行。

相关文章:

  • 【软考中级 软件设计师】计算机网络和安全
  • 软件测试外包公司测试流程分享,与企业内部测试人员的区别有哪些?
  • 【Torch学习笔记】
  • Python中的yield关键字,掌握生成器的精髓
  • linux下宝塔负载100%解决方法
  • 存储+调优:存储-IP-SAN
  • NumPy 随机数据分布与 Seaborn 可视化详解
  • 请叙述Vue 中使用了哪些设计模式
  • 安装和配置 FRP (Fast Reverse Proxy)
  • 第14章-蓝牙遥控小车 手把手做蓝牙APP遥控小车 蓝牙串口通讯讲解
  • Redis 可视化工具 RedisInsight 的保姆级安装以及使用(最新)
  • 39. 组合总和 - 力扣(LeetCode)
  • 《Ai企业知识库》-模型实践-rasa开源学习框架-基础理论-02
  • Vue3(TypeScript)-CSSProperties代码示例及用法详解
  • ubuntu22.04安装cuda10.2
  • 网络传输文件的问题
  • 收藏网友的 源程序下载网
  • 【React系列】如何构建React应用程序
  • CSS 专业技巧
  • JAVA之继承和多态
  • Less 日常用法
  • Perseus-BERT——业内性能极致优化的BERT训练方案
  • React as a UI Runtime(五、列表)
  • spring + angular 实现导出excel
  • Spring Cloud中负载均衡器概览
  • TCP拥塞控制
  • 精益 React 学习指南 (Lean React)- 1.5 React 与 DOM
  • 力扣(LeetCode)56
  • 使用 Docker 部署 Spring Boot项目
  • 小李飞刀:SQL题目刷起来!
  • 用 Swift 编写面向协议的视图
  • 原生js练习题---第五课
  • 400多位云计算专家和开发者,加入了同一个组织 ...
  • 阿里云API、SDK和CLI应用实践方案
  • 带你开发类似Pokemon Go的AR游戏
  • 昨天1024程序员节,我故意写了个死循环~
  • ​数据结构之初始二叉树(3)
  • # Redis 入门到精通(七)-- redis 删除策略
  • ###51单片机学习(1)-----单片机烧录软件的使用,以及如何建立一个工程项目
  • #stm32整理(一)flash读写
  • (1综述)从零开始的嵌入式图像图像处理(PI+QT+OpenCV)实战演练
  • (C#)Windows Shell 外壳编程系列9 - QueryInfo 扩展提示
  • (windows2012共享文件夹和防火墙设置
  • (代码示例)使用setTimeout来延迟加载JS脚本文件
  • (二)c52学习之旅-简单了解单片机
  • (二十六)Java 数据结构
  • (附源码)计算机毕业设计SSM疫情下的学生出入管理系统
  • (含react-draggable库以及相关BUG如何解决)固定在左上方某盒子内(如按钮)添加可拖动功能,使用react hook语法实现
  • (牛客腾讯思维编程题)编码编码分组打印下标(java 版本+ C版本)
  • (三)centos7案例实战—vmware虚拟机硬盘挂载与卸载
  • (十八)SpringBoot之发送QQ邮件
  • .jks文件(JAVA KeyStore)
  • .net core 使用js,.net core 使用javascript,在.net core项目中怎么使用javascript
  • .Net Core 中间件验签
  • .net 简单实现MD5