利用SpringSession实现分布式系统的跨域共享session

一. 前言

在Web项目开发中,会话管理是一个很重要的部分,用于存储与用户相关的数据。通常是由符合session规范的容器来负责存储管理,这也意味着一旦容器关闭、重启,会导致会话失效(在tomcat中,由每个context容器内的Manager对象来管理session)。

目前,流行的分布式架构下,出现了跨服务器无法共享session的问题。对此,通常的解决方案为,利用redis、JDBC等存储介质,实现session数据的持久化,进而实现分布式会话管理,具体而言,分为如下三种:

  1. 使用容器扩展来实现:比如基于Tomcat的tomcat-redis-session-manager,基于Jetty的jetty-session-redis
  1. 自己实现会话管理的工具类(包括Session管理和Cookie管理):在需要使用会话的时候都从自己的工具类中获取,而工具类后端存储可以放到Redis中【快融当前采信的方式】
  1. 使用框架的会话管理工具:即本文中的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如何传递???

  1. 针对第一个问题,Spring Session定义了一组标准的接口,可以通过实现这些接口间接访问底层的数据存储:
    • org.springframework.session.Session
      定义了session的基本功能:设置、移除属性(该接口并不关心具体存储介质),因此具有比servletHttpSession更广泛的应用场景
    • org.springframework.session.ExpiringSession
      session接口的扩展,提供判断session是否过期(一个典型的实现类RedisSession)
    • org.springframework.session.SessionRepository
      定义了创建、保存、删除以及检索session的方法,将Session实例真正保存到数据存储的逻辑是在这个接口的实现中编码完成的(一个典型的实现类RedisOperationsSessionRepository)
  1. 针对第二个问题,就HTTP协议而言, Spring Session定义了一个接口两个默认实现类:
    • HttpSessionStrategy接口
    • CookieHttpSessionStrategy实现类(使用cookie将请求与session id关联)
    • HeaderHttpSessionStrategy实现类(使用header将请求与session id关联)
  1. 相关包装类(实现对http的支持)
    • SessionRepositoryRequestWrapper
    • SessionRepositoryResponseWrapper
      Spring-session对HTTP的支持是通过标准的servlet filter来实现的,这个filter必须要配置为拦截所有的web应用请求,并且它应该是filter链中的第一个filter。Spring Session filter会确保随后调用javax.servlet.http.HttpServletRequest的getSession()方法时,都会返回Spring Session的HttpSession实例,而不是应用服务器默认的HttpSession
  1. 过滤器
    • 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
2
3
4
5
6
7
8
9
<!—注意Spring Boot版本 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>

2.添加application配置项

配置文件application.properties

1
2
3
4
5
6
########## springSession相关配置 ##########
#session过期时间(秒)
spring.session.store-type=redis
# session最大超时时间(分钟),默认为30
【如果新建了RedisSessionConfig类,该项配置失效,需在配置类上以注解形式配置过期时间】
server.session.timeout=60

3.添加配置类

  • 基本配置,配置类开启Redis Http Session
1
2
3
4
@Configuration
@EnableRedisHttpSession //开启redisHttpSession
public class HttpSessionConfig {
}
  • 进阶配置:

  • 由于快融前端项目存在跨域情况,默认的方式sessionId获取为null,导致每次生成全新的session对象,需配置 自定义的 CookieSerializer 来指定配置信息:

  • MyDefaultSessionCookieConfig:自定义CookieSerializer 所有配置项

  • HttpSessionConfig:自定义sessionId获取策略(快融项目中基于cookie策略)

4. 设置过滤器

主要用于兼容快融项目当前的redis管理用户信息方式,保持portalkey管理的用信息的生命周期,与session同步【具体参见ApiOriginFilter类】

4. 前端处理

前端处理主要分为两部分 (对封装的公共ajaxRequest进行修改):

  • ajax跨域允许携带cookie:withCredentials属性
  • ajax中ession过期的后处理:dataFilter方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
$.ajax({
xhrFields:{withCredentials:true}, //当前请求为跨域类型时是否在请求中协带cookie
url: defaults.url,
type: defaults.type,
data: defaults.data,
async: defaults.async,
cache: defaults.cache,
dataType: defaults.dataType,
contentType: defaults.contentType,
beforeSend: function (XHR) {
if(defaults.isHaveHeader){
XHR.setRequestHeader("userId", userId);
XHR.setRequestHeader("token", token);
XHR.setRequestHeader("tenantId", tenantId);
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
console.log("错误信息" + errorThrown.toString());
},
dataFilter: function(data) { //监听session过期,清除用户信息,返回登录页
if((data.status == false || data.status == "false") && data.msg == "会话过期"){
portal.util.alertBox("会话过期");
}else{
return data;
}
},
success: defaults.success
});

四. 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
2
3
4
5
// 自定义session过期时间(单位s)
@EnableRedisHttpSession(redisNamespace="spring:gateway:session:NewsEditing-cas", maxInstanceIntervalSeconds = 60)
public class HttpSessionConfig implements ApplicationContextWare {

}

4. Session何时进行更新

Session 的更新由 SessionRepositoryFilter过滤器完成,每次请求均会自动更新session过期时间

坚持原创技术分享,您的支持将鼓励我继续创作!