一. 前言
在Web项目开发中,会话管理是一个很重要的部分,用于存储与用户相关的数据。通常是由符合session规范的容器来负责存储管理,这也意味着一旦容器关闭、重启,会导致会话失效(在tomcat中,由每个context容器内的Manager对象来管理session)。
目前,流行的分布式架构下,出现了跨服务器无法共享session的问题。对此,通常的解决方案为,利用redis、JDBC等存储介质,实现session数据的持久化,进而实现分布式会话管理,具体而言,分为如下三种:
- 使用容器扩展来实现:比如基于Tomcat的tomcat-redis-session-manager,基于Jetty的jetty-session-redis
- 自己实现会话管理的工具类(包括Session管理和Cookie管理):在需要使用会话的时候都从自己的工具类中获取,而工具类后端存储可以放到Redis中【快融当前采信的方式】
- 使用框架的会话管理工具:即本文中的springSession,基于spring框架,利用spring-data-redis连接池,替换servlet中的会话管理,实现脱离容器而不用改变代码
二. Spring-session简介
1. 简介
简而言之,spring Session 提供了 一套API 和实现,用于管理用户的 Session 信息,基于此概念,其具有如下特性:
- Session持久化在外部存储介质中,通过配置自行切换(redis,mongo, Apache Geode)
- 控制 sessionid 如何在客户端和服务器之间进行交换,便于编写 Restful API (从 HTTP 头信息中获取 sessionid ,而不必再依赖于 cookie)
- 非 Web 请求的处理代码中,能够访问 session 数据
- 支持每个浏览器上使用多个 session
2. 模块
Spring-session具体提供了如下四个模块:
3. 关键接口/类
实现session管理器的时候,有两个必须要解决的核心问题:如何(利用外部存储介质)构建集群环境下高可用的session,对于传入的请求该如何确定该用哪个session实例。归根结底,关键问题在于sessionId如何传递???
- 针对第一个问题,Spring Session定义了一组标准的接口,可以通过实现这些接口间接访问底层的数据存储:
- org.springframework.session.Session
定义了session的基本功能:设置、移除属性(该接口并不关心具体存储介质),因此具有比servletHttpSession更广泛的应用场景
- org.springframework.session.ExpiringSession
session接口的扩展,提供判断session是否过期(一个典型的实现类RedisSession)
- org.springframework.session.SessionRepository
定义了创建、保存、删除以及检索session的方法,将Session实例真正保存到数据存储的逻辑是在这个接口的实现中编码完成的(一个典型的实现类RedisOperationsSessionRepository)
- 针对第二个问题,就HTTP协议而言, Spring Session定义了一个接口两个默认实现类:
- HttpSessionStrategy接口
- CookieHttpSessionStrategy实现类(使用cookie将请求与session id关联)
- HeaderHttpSessionStrategy实现类(使用header将请求与session id关联)
- 相关包装类(实现对http的支持)
- SessionRepositoryRequestWrapper
- SessionRepositoryResponseWrapper
Spring-session对HTTP的支持是通过标准的servlet filter来实现的,这个filter必须要配置为拦截所有的web应用请求,并且它应该是filter链中的第一个filter。Spring Session filter会确保随后调用javax.servlet.http.HttpServletRequest的getSession()方法时,都会返回Spring Session的HttpSession实例,而不是应用服务器默认的HttpSession
- 过滤器
- SessionRepositoryFilter
通过实现ServletFilter接口,创建3)中请求与响应对象,然后调用其余的filter,实现获得的session对象均为springsession的实例
4. 生命周期
4.1 创建session
RedisSession 在创建时设置 3 个变量 creationTime ,maxInactiveInterval ,lastAccessedTime
4.2 获取session
应用通过getSession(boolean create)方法来获取session数据【create 表示 session 不存在时是否创建新的 session】,具体步骤如下:
- getSession方法首先请求的.CURRENT_SESSION属性来获取 currentSession
- 获取不到,则从 request 取出sessionId【该步骤依赖具体的 HttpSessionStrategy 的实现】
- 然后读取 spring:session:sessions:[sessionId] 的值,同时根据 lastAccessedTime 和 MaxInactiveIntervalInSeconds 来判断这个 session 是否过期。
- 如果request中没有sessionId ,说明该用户是第一次访问,会根据不同的实现来创建一个新的 session
4.3 删除session
spring session在访问有效期内,每一次访问都会更新 lastAccessedTime 的值,过期时间为lastAccessedTime + maxInactiveInterval ,也即在有效期内每访问一次,有效期就向后延长 maxInactiveInterval,对于过期数据,一般有如下三种删除策略
- 定时删除: 即在设置键的过期时间的同时,创建一个定时器, 当键的过期时间到来时,立即删除。
- 惰性删除,即在访问键的时候,判断键是否过期,过期则删除,否则返回该键值
- 定期删除,即每隔一段时间,程序就对数据库进行一次检查,删除里面的过期键。至于要删除多少过期键,以及要检查多少个数据库,则由算法决定
5. Spring-session在 redis中的数据结构
- SET 类型的spring:session:expireations:
min 表示从 1970 年 1 月 1 日 0 点 0 分经过的分钟数, SET 集合的 member 为 expires:[sessionId] ,表示 members 会在 min 分钟后过期 - String 类型的spring:session:sessions:expires:[sessionId]:
该数据的 TTL 表示 sessionId 过期的剩余时间,即 maxInactiveInterval - Hash 类型的spring:session:sessions:[sessionId]:
session 保存的数据,记录了 creationTime,maxInactiveInterval,lastAccessedTime,attribute。前两个数据是用于 session 过期管理的辅助数据结构6.注意事项
- spring-session 要求 Redis 版本在2.8及以上
- 默认情况下,session 存储在 redis 的 key 是“spring:session::”,但如果有多个系统同时使用一个 redis,则会冲突,此时应该配置 redisNamespace 值
- 如果想在 session 中保存一个对象,必须实现了 Serializable接口
- session 的域不同会生成新的 session 的,需进行网关代理、负载均衡、或者自定义HttpSessionStrategy策略并进行跨域处理
三. Spring-session在项目中的实践
1.引入POM依赖
1 | <!—注意Spring Boot版本 --> |
2.添加application配置项
配置文件application.properties
1 | ########## springSession相关配置 ########## |
3.添加配置类
- 基本配置,配置类开启Redis Http Session
1 | @Configuration |
进阶配置:
由于快融前端项目存在跨域情况,默认的方式sessionId获取为null,导致每次生成全新的session对象,需配置 自定义的 CookieSerializer 来指定配置信息:
MyDefaultSessionCookieConfig:自定义CookieSerializer 所有配置项
HttpSessionConfig:自定义sessionId获取策略(快融项目中基于cookie策略)
4. 设置过滤器
主要用于兼容快融项目当前的redis管理用户信息方式,保持portalkey管理的用信息的生命周期,与session同步【具体参见ApiOriginFilter类】
4. 前端处理
前端处理主要分为两部分 (对封装的公共ajaxRequest进行修改):
- ajax跨域允许携带cookie:withCredentials属性
- ajax中ession过期的后处理:dataFilter方法
1 | $.ajax({ |
四. FAQ
1. 快融(2.0.3版本)当前基于redis的方式存在什么弊端:
- 必须自定义代码实现相关redis操作,对业务代码具有侵入性
- 所有接口调用时,需另外手动调用一次更新redis的接口,增加前端代码负荷
- 前端必须利用localstorage缓存记住redis的key,由于localstorage的生命周期及作用域问题,导致浏览器关闭再打开用户登录状态仍然保持的问题
2. Spring-session在redis中为什么存在三个key:
- Sessions:记录 session 本身的数据
- Expires:标记 session 的准确过期时间
- expiration :利用set集合存储一分钟内会过期的Sessions的key, 保证 session 能够被及时删除,spring 监听事件能够被及时处理
3. 为什么利用server.session.timeout配置session过期时间无效
当使用了自定义的RedisSessionConfig类时,session过期时间将以该类注解上的配置为准。
1 | // 自定义session过期时间(单位s) |
4. Session何时进行更新
Session 的更新由 SessionRepositoryFilter过滤器完成,每次请求均会自动更新session过期时间