SpringSession深入浅出(一)
一、session来由
要谈session,一定是要说到用它带场景http协议。http协议是无状态协议,就像鱼的记忆,即使是同一浏览器给客户端给同一个服务器再来请求,已经记不起来你是谁。在互联网早期,承载网页大部分都是静态简单的信息单向传递,用户打开新闻网页浏览、结束等是一种简单的交互方式。复杂场景下, 业务是复杂的,承载业务系统和用户之间有非常频繁的交互,一个操作可能涉及到多个请求,必须让http服务器记住你是谁。于是就有了cookie和session。
cookie和session都能针对请求中的特殊值进行存储,适合于标识多次请求之间公用的内容存储和传递。但要知道区别,合理使用:
-
存储位置:cookie存储在客户端(如浏览器等);session存储在服务器端。
-
空间限制:cookie由于所有访问网站都会使用客户端的公用空间,针对单条cookie的value大小和cookie总的条数都是有限制的,超出部分根据不同客户端策略会有不同的处理。例如某个泛域名由于cookie的个数很多,造成存储用户信息的cookie经常被逐出,导致用户频繁登录。session的存储一般更加灵活,一般借助于更“专业”的存储,比如普通的RDB、高速缓存Redis等。
-
安全区别:cookie由于存储在客户端不够安全,在存储和传输的时候经过的环境比较复杂,可能会被截获、篡改、伪造等,一般存储需要进行加密、签名、防枚举。session存储在服务器端相对安全,可以存储的用户标识,角色、权限等相关信息公用基础信息。
-
性能区别:cookie存储在本地,可以将信息冗余到本地,减少远程调用耗时,可以考虑将常用非敏感数据进行存储减少系统负担,提高性能。session存储在远程服务端,读取、传输都会消耗资源、耗时增加。
二、打算怎么设计
如果由你来设计一个session的实现框架怎么实现呢?要有哪些数据模型、关键业务动作
由于session需要进行持久化,我们如果简化思考,我们只存储在关系型数据库RDB,应该怎么设计表呢?
应该设置两张表,一个表存储基础的session信息,另外一个表存储session对应的其他信息
-
session基本信息表
字段名 | 字段说明 | 类型 | 备注 |
session_id | 唯一ID | varchar | |
create_time | 创建时间 | datetime | |
update_time | 更新时间 | datetime | |
expire_time | 失效时间 | bigint | |
status | 状态 | varchar |
-
session存储值表
字段名 | 字段说明 | 类型 | 备注 |
session_id | session的唯一ID | varchar | |
attribute_name | 存储值的Key值 | varchar | |
atrrbutie_value | 存储的Value值 | varchar | |
以上就是我们针对一个session的非常简单的初始设计思路。没错,spring-session也是以此为核心进行展开设计的。
三、spring-session代码结构
spring-session是spring家族中针对session的系列服务模块,不需要依赖于web容器,支持集群情况下的会话管理。
-
支持替换应用Tomcat中的HttpSession
-
接受webSocket消息使HttpS额生死哦你保持状态
-
与应用容器无关的方式替换spring webflux
-
提供和spring-security的更方便的集成
spring-session的代码部分包括:
-
spring-core:用于定义主要的接口、模型、事件、注解、基础配置模型
-
spring-session-data-mongodb:mongodb实现的session存储管理
-
spring-session-data-redis:利用redis实现的session管理
-
spring-session-jdbc:实现了利用关系型数据来实现的session存储管理
-
spring-session-hazelcast:演示了javaEE应用程序中如何与Hazelcast结合使用
对比从我们自己的设计,来和spring-session比较一下,看看spring-session从设计的角度是如何一步步演进。
Session模型设计,总体和我们之前设计的出入不大:
-
SessionRepository:Session的管理进行抽象,定义了session的新建、查找、删除的基本功能;
-
FindByIndexNameSessionRepository:为什么会扩展一个IndexName的Repository呢?提供了一种查找具有给定索引名称和索引值的所有会话的方法。作为所有提供的FindByIndexNameSessionRepository实现都支持的常见用例,有一种便捷的方法可以为特定用户查找所有会话。通过确保使用用户名填充名称.
-
ReactiveSessionRepository:这个用于非阻塞和反应式进行操作session。
-
HazelcastIndexSessionRepository:Hazecast实现
-
RedisIndexSessionRepository:高速缓存Redis的实现,存储调用spring-data-redis进行redis操作
-
MongoIndexSessionRepository:Mango数据库实现,存储调用spring-data-mongodb进行操作
-
JDBCIndexSessionRepository:RDB关系型数据库实现,支持几种主流的数据库实现Orcal、db2、mysql、SQLServer、PostgreSql。
四、产品架构
五、重点介绍几个技术点
1、如何和SpringSession集成
SessionRepositoryFilter:
SpringSession通过继承OncePerRequestFilter进行,spring框架的针对rpc请求的责任链注册进去的。filter的order需要保证考前执行。
DEFAULT_ORDER = Integer.MIN_VALUE + 50;
在链路中做了三件事情,
-
requst增强为SessionRepositoryRequestWrapper,主要增加的功能为:
-
重写session操作
-
提交:commitSession
-
更新:setCurrentSession
-
查找:getSession
-
其他:
-
chanageSessionId:防止黑客拿到sessionID,进行身份冒充。通过change方式通知相关方刷新sessionId
-
-
-
-
response增强
-
提交session
但是为什么要继承OncePerRequestFilter呢?直接注册普通的Filter行不行?
OncePerRequestFilter
OncePerRequestFilter,这个的作用是确保filter只执行一次,这个filter虽然长的和spring-web的一样,但这个是springweb框架的简化版,确保每个请求只执行一次。
/** * Allows for easily ensuring that a request is only invoked once per request. This is a * simplified version of spring-web's OncePerRequestFilter and copied to reduce the foot * print required to use the session support. * * @author Rob Winch * @since 1.0 */
关于spring-web中OncePerRequestFilter。
Filter base class that aims to guarantee a single execution per request dispatch, on any servlet container. It provides a doFilterInternal method with HttpServletRequest and HttpServletResponse arguments. As of Servlet 3.0, a filter may be invoked as part of a REQUEST or ASYNC dispatches that occur in separate threads. A filter can be configured in web.xml whether it should be involved in async dispatches. However, in some cases servlet containers assume different default configuration. Therefore sub-classes can override the method shouldNotFilterAsyncDispatch() to declare statically if they should indeed be invoked, once, during both types of dispatches in order to provide thread initialization, logging, security, and so on. This mechanism complements and does not replace the need to configure a filter in web.xml with dispatcher types. Subclasses may use isAsyncDispatch(HttpServletRequest) to determine when a filter is invoked as part of an async dispatch, and use isAsyncStarted(HttpServletRequest) to determine when the request has been placed in async mode and therefore the current dispatch won't be the last one for the given request. Yet another dispatch type that also occurs in its own thread is ERROR. Subclasses can override shouldNotFilterErrorDispatch() if they wish to declare statically if they should be invoked once during error dispatches. The getAlreadyFilteredAttributeName method determines how to identify that a request is already filtered. The default implementation is based on the configured name of the concrete filter instance. Since: 06.12.2003 Author: Juergen Hoeller, Rossen Stoyanchev
3、Redis实现
利用redis实现session的存储方式,使用模式比较常用,这里重点关注一下
redis存储sessionKey的设计
发起一条http请求到spring框架,这个时候框架会写入session信息,举例:
HMSET spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe creationTime 1404360000000 maxInactiveInterval 1800 lastAccessedTime 1404360000000 sessionAttr:attrName someAttrValue sessionAttr:attrName2 someAttrValue2
EXPIRE spring:session:sessions:33fdd1b6-b496-4b33-9f7d-df96679d32fe 2100
APPEND spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe ""
EXPIRE spring:session:sessions:expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe 1800
SADD spring:session:expirations:1439245080000 expires:33fdd1b6-b496-4b33-9f7d-df96679d32fe
EXPIRE spring:session:expirations1439245080000 2100
意味着:
- 创建了一条33fdd1b6-b496-4b33-9f7d-df96679d32fe的session
- 这个session的创建时间为1404360000000(从19701.1年开始)
- 这个session有两个属性attrName,attrName2;
redis的几个关键Key:
- spring:session:sessions:
- 用于存储每一个session的内的各种属性
- 过期时间为都会+5分钟,便于在过期后还能访问到session的相关内容。
You will note that the expiration that is set is 5 minutes after the session actually expires. This is necessary so that the value of the session can be accessed when the session expires. An expiration is set on the session itself five minutes after it actually expires to ensure it is cleaned up, but only after we perform any necessary processing.
- findById:会针对获取的session进行有效期判定,如果过期默认不返回(接口参数开关可以设置是否返回过期session)
- spring:session:sessions:expires
- 用来存储一个Key对应的空的缓存
- 在过期的时候,会产生过期事件,但是无法保证key的过期时间抵达后立即生成过期事件。spring-session为了能够及时的产生Session的过期时的过期事件
- spring:session:expirations
- 过期key的一个“筒”,每分钟生成一个,将这一分钟之内生成新的sessionId全部放在这个里面。
- spring-session中有个定时任务,每个整分钟都会查询相应的spring:session:expirations:整分钟的时间戳中的过期SessionId,然后再访问一次这个SessionId,即spring:session:sessions:expires:SessionId,不做删除只查询一次,触发redis删除cookie,以便能够让Redis及时的产生key过期事件
事件:
springSession提供针对session生命周期的扩展点,用户可针对session的变化定义上层业务行为。通过spring-data-redis的RedisMessageListenerContainer进行事件监听。事件包括:
- SessionCreatedEvent:创建session时的事件
- "spring:session:channel:created:${sessionId}"发送redis事件,服务器集群内机器将收到SessionCreatedEvent事件
- SessionDestroyedEvent:通过destroy方法产生的事件
- SessionDeletedEvent:通过删除产生的事件
- SessionExpiredEvent:session过期产生事件
上述事件,在一些长连接,例如:Spring Session's WebSocket场景中,可以通过在session过期的时候及时关闭连接。更多redis事件信息看这里。
如何关闭事件
ConfigureNotifyKeyspaceEventsAction用于注册redis的事件中,集成于ConfigureRedisAction。如果要关闭事件机制,需要实现固定ConfigureRedisAction设置这个Bean为NO_OP就可以
@Bean
public static ConfigureRedisAction configureRedisAction() {
return ConfigureRedisAction.NO_OP;
}
XML定义Bean
<util:constant
static-field="org.springframework.session.data.redis.config.ConfigureRedisAction.NO_OP"/>
4、更详细的技术点介绍
六、其他
1、session和Tomcat
StandardSession来定义的session主要模型,通过实现Session、Serializable、HttpSession等几个接口,通过外观模式,封装session的基础服务。
spring的attribute存在ConcurrentHashMap中
/** * The collection of user data attributes associated with this Session. */protected Map<String, Object> attributes = new ConcurrentHashMap<String, Object>();
tomcat是如何做session 同步的? 如果不是单体应用,一个应用在集群的多台机器中,如果session各自独立,就无法实现,这个时候就需要进行会话保持。会话保持有几种方式:
负载均衡会话保持
-
简单会话保持:根据访问请求的源地址作为判断关连会话的依据,对来自同一IP地址的所有访问请求在作负载均时都会被保持到一台服务器上去。
-
cookie模式:将会话标识在后端服务器中间进行读取、插入;
-
SSL sessionId模式:SSL环境中,在建立SSL中插入SSL SessionID信息进行会话保持
关键服务模型
DeltaSession
主要目的是用于集群间同部的session模型,除了实现上述的StandardSession之外,还实现了Externalizable、ClusterSession、ReplicatedMapEntry三个接口,特别实现了集群间同步服务、比较服务等。
一个请求作为一个完整颗粒度,修改属性的操作会放在指定的队列中,然后将其同步到集群其他节点,节点一次执行本次针对会话属性的操作。
StandardManager
标准的实现session的服务,提供一个专门管理某个web应用所有会话的容器,并且会在web应用启动停止时刻进行会话重加载和持久化。例如会话id生成、根据会话id找出对应的会话、对于过期的会话进行销毁等等操作。
DeltaManager
DeltaManager会话管理器是tomcat默认的集群会话管理器,它主要用于集群中各个节点之间会话状态的同步维护。会话同步通信解决方案,写入完成后才会相应客户端。为区分不同的动作必须要先定义好各种事件,例如会话创建事件、会话访问事件、会话失效事件、获取所有会话事件、会话增量事件、会话ID改变事件等等,实际上tomcat集群会有9种事件
PersistentManager
用于存储session的管理服务,实现各种会话存储方式。作为存储设备最重要的操作无非就是读写操作,读即是将会话从存储设备加载到内存中,而写则将会话写入存储设备中,所以定义了两个重要的方法load和save与之相对应。FileStore和JDBCStore只要扩展Store接口各自实现load和save方法即可分别实现以文件或数据库形式存储会话。