Spring-Session 知识点 和 源码分析(上)
一、什么是 session ? 每一个用户 只拥有一个 session 。
(1)Session 的 作用 就是 让我们的 服务器记住 是哪个用户,也就是浏览器访问过我。当我发起一个 会话,我们的服务器端 就会 为我们产生 session,每一个session对应一个sessionID,然后将这个sessionID存放在cookie中然后将cookie存放在我们的浏览器中,然后我们在不关闭浏览器的 条件下,每次发送请求的时候,会默认的无条件的 将我们的 对应这个服务器的 cookie 重新发送给我们的 服务器,这样我们的 服务器就重新难道了 存放在cookie中的sessionID,从而就找到了对应的session,这样就实现了服务器记住我们用户。 我们 在 很多 情况 都会 使用过 cookie 来跟踪 我们的 session,用cookie来存放 sessionID实现跟踪。
每一个用户(浏览器) 都对应一个 唯一的独有的 sessionID,通过这个sessionID可以找到唯一对应的 session,我们浏览器的cookie中保存着 第一次 会话 由 服务器传递过来的sessionID。
【URL重写技术 解决 我们的session追踪问题】:
如果 我们的 浏览器 禁止 了 向 cookie 中存放数据,那么我们的 服务器 就没有办法 向我们的 cookie中存放 sessionID,那样我们怎样实现 对 session的 跟踪 ?
解答: 这里我们使用到了一个 【URL重写】技术 。
这个技术 非常好理解: 就是在我们每次发送请求的时候,我们的浏览器会将从服务器哪里接收到的 sessionID 当做一个 请求参数 挂在请求的 最后,如:
Http://localhost:8080/myweb/index.jsp?sessionId=xxxxx 。
这个参数 在我们的 服务器端 会 被接收,我们服务器拿到这个参数之后,用来查找判断这个sessionId对应哪个session。
【session 的管理】
(1)session 通常是 交给我们的 tomcat容器管理的。因为我们的tomcat是用来处理动态资源的,所以我们得tomcat中的一个容器专门用来存放我们的 session 。
(2)但是我们的 实际开发中我们不单单只涉及一个 tomcat服务器了,我们是多个tomcat服务器集群的,而且 我们用户和tomcat之间还存在一个nginx,这个nginx是用来做 负载均衡的。这时候就出现了一个问题:
我们的 nginx负载均衡如果不做任何的配置,nginx接收到的请求分发到的tomcat可能是不相同的,因为nginx是根据tomcat的压力和一定的规则来进行分发请求的,所以我们用户 每次发出的请求 处理请求的 tomcat不一定是相同的。
但是我们的 每一个tomcat中都部署 相同的 web项目,所以我们的 用户 在每次 访问我们的页面的时候 虽然是不同的 tomcat进行处理,但是显示的网站是相同 的。
因为每次 处理请求的tomcat 是不相同的,就会造成一个问题:session的丢失和不能共享的问题:
因为每次由nginx负载均衡的原因分到的tomcat不相同,所以每次都找不到上一次请求会话sessionId对应的session,所以导致这次的tomcat认为这个请求是新的请求,也是新的一次会话,所以产生新的 session,然后返回新的sessionId,然后代替掉浏览器中的cookie中的sessionId,这样就 每次 用户登录之后 发出一个请求然后刷新一下页面就退出了,就变成了登录又退出登录退出这样的情况。 这就是tomcat集群部署之后存在的问题。
****我们怎样解决tomcat集群部署和nginx负载均衡产生的 session不共享丢失的问题呢?
【第一种方案】:ip_hash策略:这个策略是 我们nginx负载均衡时在nginx.Conf配置文件下配置的 请求分配策略。
Ip_hash的原理: 一个ip地址绑定一个tomcat服务器。其中涉及一个计算公式:
Hash函数( ip地址 ) % 2 == 取余,余数对应tomcat的编号。
所以只要ip地址不变 ,对应的 tomcat编号就不变。
但是 这种 解决 session丢失问题的 方法不好:因为 这样我们的ip地址就不能变了,如果我们今天从山东用的山东移动,明天去了河北,用的河北移动,这样session就没了,意思是ip地址不能改变的,如果你ip变了,就有可能调到了另一个服务器中去了,左脚在北京 右脚进了河北,ip变化了,然后跳转tomcat,然后用户就丢失了session,就退出登录了。
【第二种方案】:Spring Session 框架。
这种技术 可以使 session 同步 ,可以对session进行管理,解决集群部署之后 产生的session丢失不同步的 问题。
*****它是怎么实现的?
我们的spring session 以中立的身份,取代和代替 我们 tomcat中的 httpsession,然后将session中的信息数据 存放在 redis 中。
知识点:为什么存放在redis中呢?
我们的redis是将数据存放在内存中的,而且我们可以给数据设置存放的最大生命时间,达到最大生命时间之后就会被自动删除,这一点和我们的session的最大空闲时间是相同相似的。而且我们从内存中读取数据是很快的。
作用步骤:
(1)用自身的 Spring-Session 以中立的方式 替换我们的 http-Session。
(2)然后 将 session中的数据信息 存放进 redis 中 。
(3)只需要配置 不需要 写任何的代码,就可以实现springsesson功能。
开发步骤:使用springsession开发步骤。
1.添加springsession的 maven 依赖。
1)
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
这个依赖 是对我们的 spring-data-redis依赖的封装。这是依赖是我们springsession所依赖的jar包 。
2)
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-redis</artifactId>
<version>1.8.8.RELEASE</version>
</dependency>
这个依赖 是我们 在 spring框架中 【操作redis】时 所需要的依赖。
3)
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
<version>2.9.0</version>
</dependency>
我们的【spring-data-redis】这个依赖 在操作 redis的时候,底层所依赖的jar包是jedis包,实际上 我们【spring-data-redis】包底层是通过【jedis】包来操作我们的redis的。
4)
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.13.RELEASE</version>
</dependency>
我们的spring-session是在web项目中使用,所以涉及到了web项目和spring,所以需要使用【spring-web】这个依赖。
5)我们在开发web项目时,所需要的一些依赖,可加可不加,加了有提示不报错。
<!-- servlet依赖的jar包start -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version>
</dependency>
<!-- servlet依赖的jar包start -->
<!-- jsp依赖jar包start -->
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>javax.servlet.jsp-api</artifactId>
<version>2.3.1</version>
</dependency>
<!-- jsp依赖jar包end -->
<!--jstl标签依赖的jar包start -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!--jstl标签依赖的jar包end -->
分别是【servlet依赖】,【jsp依赖】 和 【jstl依赖】。
2.我们在web.xml文件文件中配置springSessionRespositoryFilter过滤器。
SpringSessionRespositoryFilter:springSession仓库过滤器。
我认为这个过滤器的作用是:将 我们 所有的 httpSession替换成 springsession,然后将我们session中的信息数据存放在redis中。
applicationContext-session.xml配置文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- spring注解、bean的处理器 -->
<!--这个标签 是用来 **注解 的-->
<context:annotation-config/>
<!-- Spring session 的配置类 -->
<!--RedisHttpSessionConfiguration这个类专门是用来处理 redis 和 httpsession之间的关系的类-->
<!--
我们的RedisHttpSessionConfiguration 这个类中 有很多很多的注解,但是 RedisHttpSessionConfiguration里面的这些方法上类上的注解并没有被**。
所以我们需要 <context:annotation-config/> 这个标签**一下 我们 我们的RedisHttpSessionConfiguration类中的注解。
如果我们不进行 注解**,那么我们的RedisHttpSessionConfiguration是不能使用的,会报错。
-->
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
<!--配置 redis的连接时 所需要的 信息 的-->
<!--
spring 封装了 RedisTemplate 对象来进行对redis的各种操作,它支持所有的 redis 原生的 api。
-->
<!--
读取redis.properties属性配置文件
在我们的 applictionContext-session.xml中引入redis的配置文件,然后就可以使用EL表达式进行赋值了。
-->
<context:property-placeholder location="classpath:redis.properties"/>
<!--下面的配置是为了告诉我们的springsession怎么连接redis的,只需要告诉我们的springsession怎么连接redis,我们都不需要创建RedisTemplate 对象 -->
<!-- 配置jedis连接工厂,用于连接redis -->
<bean id="jedisConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
<property name="hostName" value="${redis.hostName}"/>
<property name="port" value="${redis.port}"/>
<property name="password" value="${redis.password}"/>
<property name="usePool" value="${redis.usePool}"/>
<property name="timeout" value="${redis.timeout}"/>
</bean>
</beans>
redis.properties :
redis.hostName=192.168.xxx.xxx
redis.port=6379
redis.password=xxxx
redis.usePool=true
redis.timeout=15000
Web.xml :
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
id="WebApp_ID" version="3.0">
<!--SpringSessionRespositoryFilter 这是一个spring-session框架的全局过滤器-->
<!--org.springframework.web.filter.DelegatingFilterProxy就是我们的SpringSessionRespositoryFilter的jar包-->
<filter>
<filter-name>springSessionRepositoryFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSessionRepositoryFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!--
(1)因为涉及到了spring,所以我们需要spring配置文件,所以需要使用 contextLoader监听器,来监视我们context创建的时刻。
(2)在context创建的时刻,就将我们的spring配置文件 读取和加载进我们的内存,然后spring容器中就创建好我们需要的实体对象了。
(3)使用contextLoaderListener 就是为了加载我们的 spring配置文件 。
-->
<!-- needed for ContextLoaderListener -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<!-- Bootstraps the root web application context before servlet initialization -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
</web-app>
我们的session 通过 Spring-session这个框架存放到了我们的redis中。我们session默认最大空闲时间为30minute。
这个类因为有这个注解,所以它有一个定时任务的功能。每个段时间就检查一下我们的session最后一次访问时间,看看我们在最后一次访问之后,有没有过30分钟啊,如果30分钟都没有访问过:
就利用 圈1 和 圈3 这两个 key配合起来 对我们 圈1,2,3 这三个key的删除。
我们真正的session存放在我们的 圈2里面,我们圈1 和圈3 是配合起来在session失效的时候删除这三个key的。
Redis中 存放的资源类型:
- 动态资源 或 常量:因为我们的 Redis管理数据是以键值对的方式进行管理的。 但是存放的 动态资源和常量我们一般都设置最大生命时间,所以说 在redis中存放的数据 都是不持久的动态资源,虽然可以长久的存放,但是我们人为的设置了最大生命时间了。
- 利用spring-session框架 存放我们的 session对象,并且存放的session对象最大空闲会话时间为30分钟。
********为什么 我们的 spring-session 框架 可以 让我们的 session对象共享?
由上面 关系图 我们可以看出 :
(1) redis是在【所有】的tomcat服务器的后面。
(2) redis中 存放了所有的 session对象 。
(3)所有的 tomcat 想获取 session 都需要去 redis中获取。
(4)tomcat 从 redis 中 获取到 session 的过程 不需要我们参与,全权由我们的Spring-session操作完成,spring-session帮我们将session拦截到然后存放进我们的 redis中,然后我们想要获取的时候,spring-session通过浏览器传递过来的cookie中的sessionId来自动从redis中帮我们查找对应sessionId的session。
总结:
之所以我们的session共享了,是因为 我们将所有的session存放进了一个公共的空间了,所有的tomcat都可以随便的自动的从这个公共空间获取session了。 原来没有配置springsession的时候 我们 的session是存放在 tomcat中的容器里,这个容器是封闭的,是与其他tomcat没有任何交涉的,所以session不共享。
*********我们判断一个用户是否登陆的依据是什么?
我们的用户登录之后 我们的tomcat 就会创建一个session对象,这个session对象是和我们的用户一一对应的,session里面存放着这个用户的所有信息。当我们的用户浏览器发送请求后,请求携带者cookie中的sessionId到达我们的服务器,然后我们的tomcat服务器从redis中获取 sessionId对应的session,如果找到了 说明 这个用户是登陆状态就显示登陆,如果没有找到对应的session对象,说明该用户没有登录,就显示未登录状态。
Path是cookie的路径不相同,产生两个session:
P2p项目
Show项目
因为name相同,如果path也相同,那么他们就是同一个session了,键值对嘛,键不能重复。
让不同的项目 共享 session 的 方法 :
指定cookie的存储方式:
利用下面这个类来进行序列化,修改cookie的存储格式:
这个类如何序列化,修改cookie存储格式的:
上面这个类实现了这个接口。
配置完成之后:
P2p项目:
Show项目:
这两个项目都是有同一个浏览器用户发出的请求,所以他们的session应该保持一致,如果不一致,就用这种方法让session在两个项目或多个项目中保持共享。
域名不相同,项目不相同,怎么实现session共享:
让两个项目的session保存在同一个路径下:
这两个项目的根域名相同,所以都存放在根域名下,路径为:根域名/
分别制定:domain 和 path这两个值。
Web.com是 一级域名。
项目名 为 二级域名。
包结构:
--Web
--Com
--p2p
--show
上面这个地址 是 存放cookie的地址。
Cookie中是用键值对来保存 sessionId的。
Name就是key:session 就代表sessionId
Value 里面就存放着sessionId的值。
Domian + path 就是存放cookie的位置。
强行将 cookie放在abc.com这个域名下,这样违反了cookie安全策略,在写到浏览器的时候 会 被禁止,无法写到浏览器上。Cookie只能保存在当前域名下: cookie的安全策略不允许写到别的域名下。
这种情况怎么实现 一个网站登录其他信任网站也登陆呢?
单点故障:如果只有一个节点一个服务器,如果这个节点服务器发生故障了,整个项目网站服务就不能用了。所以至少布置两台服务器节点,但是两台就会出现session不共享的问题,所以我们需要 使用springsession框架实现我们的session共享。
知识点补充:
Init是初始化方法。以前出现的init方法都是初始化方法。
判断 日志级别是否为 debug级别,如果是debug级别,就打印日志。
Init初始化方法中的核心代码:
代理过滤器中重写这个方法:
GetFilterName()方法底层代码:
意思是:从配置文件中获取到<filter-name>之间的值。
获取的是标签对之间的值:
获取 相同名字,相同类型过滤器所涉及的方法 :
我们 spring容器中获取到了过滤器的实体bean对象:
<bean id=”springSessionRepositoryFilter” class=”SessionRepositoryFilter的全限定类名”/>
我们调用的是springSessionRepositoryFilter对象中的 doFilter()方法,实质上其实是调用的SessionRepositoryFilter类中的doFilter()方法。
而我们的SessionRepositoryFilter类 中的doFilter()是从我们的OncePerRequestFilter抽象类中继承过来之后重写的,OncePerRequestFilter抽象类中的doFilter()是从我们的Filter接口中得到的。
调用 dofilter()方法的过程:
这个Dofilter方法中的核心代码为:
这个方法在我们的 中被重写:代码内容如下:
是这个doFilter()方法的核心代码,因为这段代码和管理session,存储session有关:
这个内部类 继承了 父类中的 getSession方法,并且重写了这个方法。
重写的是【父类】这里面的这些方法:
重写的getSesion方法中的代码内容:核心代码红线(创建一个session对象)
创建session对象的方法的代码内容如下:
创建session对象的方法中的核心代码:
我们将session存放进redis中。
添加属性,如下图:向Map集合中添加属性就相当于下图。redis中的效果图:
保存session的方法:
上图doFilter()方法中有下图这一段代码,专门是用来存储session的:
CommitSession方法代码内容如下:
核心代码中save方法内容如下:
Save核心代码 中 saveDelta()方法内容如下:
这个putAll()方法属于我们的spring-data-redis依赖中的方法,这里已经到了 spring-data-redis中了:
我们spring-data-redis依赖底层向redis存放数据又要依赖 jedis的jar包:
Jedis帮我们保存 session,jedis涉及的方法如下:
上面的源码就是这个图的步骤和知识点。
方法就等于复写了
方法。
知识点补充:我们不从配置文件中声明bean对象,我们用java代码来实现创建bean对象。
上面 两种方式 都是相当于在 spring容器中 配置和声明了一个 bean对象。
实际开发中,命名 redis中key的规范: