转载

分布式场景下Session解决方案-SpringSession

摘要: 本文主要研究基于 spring-seesion 解决分布式 session 的共享问题。首先聊一下session与cookie的作用与工作原理,然后步入主题,讲述session 共享问题的产生背景以及常见的解决方案;接着讲述了 spring-session 的两种管理 sessionid 的方式以及对应的使用场景;再接着对后台保存数据到 redis 上的数据结构进行了分析;然后对 spring-session 的核心源代码进行了解读,方便理解 spring-session 框架的实现原理。

一:Session与Cookie

1.Cookie:

因为HTTP协议是无状态的,即服务器不知道用户上一次做了什么,无法创建同一用户请求的关联性,因此需要浏览器提供一个机制供服务端识别,这时Cookie便出现了。 通过引入cookie和session体系机制来维护状态信息。即用户第一次访问服务器的时候,服务器响应报头通常会出现一个Set-Cookie响应头,这里其实就是在本地设置一个cookie,当用户再次访问服务器的时候,http会附带这个cookie过去,cookie中存有sessionId这样的信息来到服务器这边确认是否属于同一次会话。

Cookie的属性:

name:cookie的名字,Cookie一旦创建,名称便不可更改

value:cookie值

comment:该Cookie的用处说明。浏览器显示Cookie信息的时候显示该说明。

domain:可以访问该Cookie的域名。如果设置为“.baidu.com”,则所有以“baidu.com”结尾的域名都可以访问该Cookie;也就是只有一级域名一致的情况下才可以访问同一cookie。

maxAge:Cookie失效的时间,单位秒。

正数,则超过maxAge秒之后失效。 负数,该Cookie为临时Cookie,关闭浏览器即失效,浏览器也不会以任何形式保存该Cookie。 为0,表示删除该Cookie。 path:该Cookie的使用路径。例如:

path=/,说明本域名下contextPath都可以访问该Cookie。

path=/app/,则只有contextPath为“/app”的程序可以访问该Cookie。

path设置时,其以“/”结尾。

secure: 该Cookie是否仅被使用安全协议传输。这里的安全协议包括HTTPS,SSL等。默认为false。

Cookie是不支持跨域的,对于Cookie来说,Cookie的同源只关注域名,是忽略协议和端口的。所以一般情况下, https://localhost:80/和http://localhost:8080/的Cookie是共享的。

Cookie是不可跨域的;在没有经过任何处理的情况下,二级域名不同也是不行的。(wenku.baidu.com和baike.baidu.com)。只有当domainname设置为.baidu.com时才可以访问同一cookie。

Cookie数量&大小限制及处理策略 www.cnblogs.com/henryhappie…

2.Session

Cookie机制弥补了HTTP协议无状态的不足。在Session出现之前,基本上所有的网站都采用Cookie来跟踪会话。 与Cookie不同的是,session是以服务端保存状态的。

当客户端请求创建一个session的时候,服务器会先检查这个客户端的请求里是否已包含了一个session标识 - sessionId,

如果已包含这个sessionId,则说明以前已经为此客户端创建过session,服务器就按照sessionId把这个session检索出来使用(如果检索不到,可能会新建一个) 如果客户端请求不包含sessionId,则为此客户端创建一个session并且生成一个与此session相关联的sessionId

sessionId的值一般是一个既不会重复,又不容易被仿造的字符串,这个sessionId将被在本次响应中返回给客户端保存。保存sessionId的方式大多情况下用的是cookie。

扩展:session的生命周期

session创建:在第一次使用resquest的getSession方法,web服务器会创建一个session

session使用:session在服务端创建完成后,内存会给session分配一定的空间,并且会生成一个临时cookie返回给用户,浏览器通过set-cookie创建cookie并保存到本地,此后访问都通过此cookieid找到对应的session。

session的销毁:
1.默认销毁:如果与服务端30分钟内没有交互,默认销毁。
2.手动销毁:当调用session的invalidate方法时候会销毁此session。
3.关闭服务器:内存空间被回收了,自然就不存在session了。
复制代码

解决方案-SpringSession

HttpSession 是通过 Servlet 容器创建和管理的,像 Tomcat/Jetty 都是保存在内存中的。而如果我们把 web 服务器搭建成分布式的集群,然后利用Nginx 做负载均衡,那么来自同一用户的 Http 请求将有可能被分发到两个不同的 web 站点中去。那么问题就来了,如何保证不同的 web 站点能够共享同一份 session 数据呢?

最简单的做法是把session从容器中抽离出来。 第一种是使用容器扩展来实现,大家比较容易接受的是通过容器插件来实现,比如基于 Tomcat 的 tomcat-redis-session-manager ,基于 Jetty 的 jetty-session-redis等等。好处是对项目来说是透明的,无需改动代码。不过前者目前还不支持 Tomcat 8 ,或者说不太完善。但是由于过于依赖容器,一旦容器升级或者更换意味着又得从新来过。并且代码不在项目中,对开发者来说维护也是个问题。

第二种是自己写一套会话管理的工具类,包括 Session 管理和 Cookie 管理,在需要使用会话的时候都从自己的工具类中获取,而工具类后端存储可以放到 Redis 中。很显然这个方案灵活性最大,但开发需要一些额外的时间。并且系统中存在两套 Session 方案,很容易弄错而导致取不到数据。

第三种是使用框架的会话管理工具,也就是如下介绍的 spring-session ,可以理解是替换了 Servlet 那一套会话管理,接管创建和管理 Session 数据的工作。既不依赖容器,又不需要改动代码,并且是用了 spring-data-redis 那一套连接池,可以说是最完美的解决方案。当然除了redis管理存储外,spring-session也可通过数据库通过jdbc存储

1.spring Session 提供了 API 和实现,用于管理用户的 Session 信息。除此之外,它还提供了如下特性:

2.将 session 所保存的状态卸载到特定的外部 session 存储汇总,如 Redis 中,他们能够以独立于应用服务器的方式提供高质量的集群。

3.控制 sessionid 如何在客户端和服务器之间进行交换,这样的话就能很容易地编写 Restful API ,因为它可以从 HTTP 头信息中获取 sessionid ,而不必再依赖于 cookie。

4.在非 Web 请求的处理代码中,能够访问 session 数据,比如在 JMS 消息的处理代码中。

5.支持每个浏览器上使用多个 session,从而能够很容易地构建更加丰富的终端用户体验。

当用户使用 WebSocket 发送请求的时候,能够保持 HttpSession 处于活跃状态。

方案一:由 cookie 管理 sessionid(默认管理方式) 在springboot中集成springsession非常简单,引入

分布式场景下Session解决方案-SpringSession

由于spring-session默认采用cookie管理策略,所以使用spring-session只需要在启动类添加@EnableRedisHttpSession注解,参数对应可设置session过期时间,以及redis存放空间位置,刷新模式以及定时清除。 maxInactiveIntervalInSeconds - 会话将在几秒钟后到期的时间

redisNamespace - 允许为会话配置特定于应用程序的命名空间。 Redis键和通道ID将以 spring:session:: 的前缀开头。

redisFlushMode - 允许指定何时将数据写入Redis。默认值仅在 SessionRepository 上调用 save 时。值 RedisFlushMode.IMMEDIATE 将尽快写入Redis。

分布式场景下Session解决方案-SpringSession

SpringSession 提供了CookieSerializer接口的默认实现DefaultCookieSerializer,当然在实际应用中,我们也可以自己实现这个接口,然后通过CookieHttpSessionIdResolver#setCookieSerializer(CookieSerializer)方法来指定我们自己的实现方式。

方案二:通过HttpHeader管理sessionid

spring-session支持请求头来管理session,当cookie被禁用的情况下可以通过请求头携带token来匹配对应session。Spring Session允许在 Headers 中提供会话ID以使用 RESTful APIs

下面就是如何使用这两种管理方式:SpringSession中对于sessionId的解析相关的策略是通过HttpSessionIdResolver这个接口来体现的。HttpSessionIdResolver有两个实现类:

分布式场景下Session解决方案-SpringSession

`public interface HttpSessionIdResolver {

List<String> resolveSessionIds(HttpServletRequest request);
void setSessionId(HttpServletRequest request, HttpServletResponse response,String sessionId);
void expireSession(HttpServletRequest request, HttpServletResponse response);
复制代码

}

resolveSessionIds:解析与当前请求相关联的sessionId。sessionId可能来自Cookie或请求头。

setSessionId:将给定的sessionId发送给客户端。这个方法是在创建一个新session时被调用,并告知客户端新sessionId是什么。

expireSession:指示客户端结束当前session。当session无效时调用此方法,并应通知客户端sessionId不再有效。比如,它可能删除一个包含sessionId的Cookie,或者设置一个HTTP响应头,其值为空就表示客户端不再提交sessionId。

我们可以通过创建HttpSessionIdResolver的自定义实现类来选择合适的session管理策略。

SpringSession源码解析

在这里spring-session是通过redis来管理的,如果需要了解redis是如何操作的,就需要了解一下RedisOperationsSessionRepository这个类了。

RedisOperationsSessionRepository 是使用Spring Data的 RedisOperations 实现的 SessionRepository 。在Web环境中,这通常与 SessionRepositoryFilter 结合使用。该实现支持 SessionDestroyedEvent 和 SessionCreatedEvent 至 SessionMessageListener 。

Spring Session使用 Session 的最基本的API是 SessionRepository 。这个API有意非常简单,因此很容易提供具有基本功能的其他实现。 一些 SessionRepository 实现也可以选择实现 FindByIndexNameSessionRepository 。例如,Spring的Redis支持实现 FindByIndexNameSessionRepository 。 FindByIndexNameSessionRepository 添加了一种方法来查找特定用户的所有会话。这是通过确保使用用户名填充名称为 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 的会话属性来完成的。开发人员有责任确保填充属性,因为Spring Session不知道正在使用的身份验证机制。下面是一个如何使用它的示例:

String username = "username"; this.session.setAttribute( FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME, username); FindByIndexNameSessionRepository 的某些实现将提供钩子以自动索引其他会话属性。对于例如,许多实现将自动确保使用索引名称 FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME 索引当前的Spring Security用户名。

String username = "username";

Map<String, Session> sessionIdToSession = this.sessionRepository .findByIndexNameAndIndexValue(FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME,username);

原文  https://juejin.im/post/5cf65bd2f265da1b827a892e
正文到此结束
Loading...