第一章、Session会话管理概述
Session和Cookie回顾
Session机制
由于HTTP是无状态的协议,每次浏览器与服务器的交互过程就是一次对话,对话结束之后服务器不能记住你这个人。下次对话的时候服务端无法识别你是上次的人,所以需要一种机制来记录用户的状态信息,这个机制就是session。
Cookie机制
每次HTTP请求的时候,客户端都会发送相应的cookie信息到服务端。
第一次创建Session时,服务端会在HTTP协议中向客户端 Cookie 中记录一个Session ID,以后每次请求把这个会话ID发送到服务器,这样服务端就知道客户端是谁了。
url重写机制
如果客户端的浏览器禁用了Cookie,那么就需要在请求的url地址后面添加上session的id,这样来识别。
浏览器和服务器对Session和Cookie机制的使用
发起请求
浏览器:每次发起请求都会将Cookie数据传递给后端的tomcat服务器。这个Cookie中存放的就是一个类似于map集合的,sessionid的值。
tomcat:tomcat启动之后会启动一个jvm虚拟机,里面存放着Session的id和对应的内存地址。
如果用户不是第一次请求,将浏览器传递过来的cookie中的sessionid取出来找到对应的内存地址并找到session对象。这样就可以追踪用户的状态信息。
如果用户是第一次请求,那么浏览器传递过来的cookie中什么都没有,这样tomcat就会创建一个session和对应的id,分别存放在jvm和返回到浏览器。
Session会话管理带来的问题以及解决方案
session会话的问题
在web的实际开发过程中,session是交给tomcat容器管理的,里面记录着用户的状态以及相关的数据。
在单击版本中,这种管理方式并没有问题。但是如果使用集群部署,将项目部署在多台tomcat中,使用负载均衡的方式访问。那么session存放在tomcat中就会出现无法共享的问题。
session会话共享方案
使用容器的拓展插件来实现:
基于tomcat的tomcat-redis-session-manager插件;
基于jetty的jetty-session-redis插件、memcached-sessuib-manager插件
这个方案的好处对项目来说是透明的,但是依赖容器,容器升级会导致重新配置
底层原理是:复制session到其他服务器,有一定的延时,并且不能部署太多
使用nginx负载均衡的ip_hash策略实现用户每次访问都绑定到同一台服务器
局限性就是ip不能改变,但是在网络中的ip一般是DHCP的动态形式。改变地理位置也容易导致ip发生改变。
并且如果某一台服务器发生故障就会导致ip重新分配,导致访问不到原来的服务器。
使用框架会话管理工具,也就是spring session。这个方案既不依赖于tomcat服务器,又不需要改动代码,由spring session框架为我们提供。
第二章、Spring Session入门
SpringSession简介
Spring Session 是Spring家族中的一个子项目,它提供一组API和实现,用于管理用户的session信息
它把servlet容器实现的httpSession替换为spring-session,专注于解决 session管理问题,Session信息存储在Redis中,可简单快速且无缝的集成到我们的应用中;
spring session特性
提供用户session管理的API和实现
提供httpSession,以中立的形式取代web容器中的session
支持集群的session处理,不必绑定到具体的web容器去解决集群session共享问题
SpringSession的使用(简单例子)
第一步、创建一个简单的session会话例子
使用servlet创建一个get和set方法,set在session域中填入一个数据,并打印填入成功,然后get中取出并打印数据。
需要的依赖:
<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>
第二步、测试两个服务器的session是否共享
将这个项目部署到两个不同的tomcat服务器,注意服务器和jvm的端口号。第一个叫9100,第二个叫9200.
启动两个服务器,然后再9100访问set方法创建session对象,然后再9200访问get方法看看能否取到数据。
经过测试发现9200无法取到9100中存放的session信息。这时候就是两个服务器各自持有自己的session不共享导致正常的服务无法运行。
第三步、添加SpringSession集成配置
添加依赖
<!-- Spring session redis 依赖start -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
<version>1.3.1.RELEASE</version>
</dependency>
<!-- Spring session redis 依赖end -->
<!-- spring web模块依赖 start -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>4.3.16.RELEASE</version>
</dependency>
<!-- spring web模块依赖end -->
在web.xml文件中配置springSessionRepositoryFilter过滤器和加载Spring配置文件
<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>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:applicationContext.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
创建applicationContext-session.xml
A、 配置一个RedisHttpSessionConfiguration类
<context:annotation-config/>:用于激活已经在Spring容器中注册的bean或者注解,因为我们通过容器创建的bean中,底层有可能使用了其它的注解,我们通过<context:component-scan>就不能指定具体的包了,所以可以使用<context:annotation-config/>激活
<!-- spring注解、bean的处理器 -->
<context:annotation-config/>
<!-- Spring session 的配置类 -->
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
B、 配置Spring-data-redis
<!-- 配置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>
<!--读取redis.properties属性配置文件-->
<context:property-placeholder location="classpath:redis.properties"/>
c、配置redis.properties文件
redis.hostName=192.168.235.128
redis.port=6379
redis.password=123456
redis.usePool=true
redis.timeout=15000
在applicationContext.xml中导入applicationContext-session.xml
将集成spring session的xml单独写成一个然后引入spring需要的配置文件。这样整个程序的耦合性会比较低。
第四部、部署测试
重复之前的两台服务器存放和读取操作,开启linux服务器上开启的redis数据库。
在浏览器中访问tomcat9100服务器的setSession
在浏览器中访问tomcat9200服务器的getSession
部署成功。
第三章、SpringSession常见的应用场景
同域名下相同项目集群实现Session共享
准备三台tomcat,然后将一个项目p2p(服务提供者)复制成两份存放到两台tomcat部署。需要这两台服务器实现session共享。另一台服务器部署dataservice(消费者)。
实现部署
在linux上开启三台tomcat服务器,然后使用xftp将p2p项目和dataservice项目上传到tomcat下的webapps目录下。
在linux里面启动mysql数据库,使用客户端工具创建需要的数据库。
修改一下dataservice中的数据源和dubbo注册地址,修改一下p2p项目的信息。
启动zookeeper服务注册中心。
启动redis服务器。
启动nginx实现服务的反向代理并负载均衡。
upstream www.p2p.com{
server 127.0.0.1:9100;
server 127.0.0.1:9200;
}
直接测试项目发现负载均衡后登陆不成功,这是因为session负载均衡后session丢失了。
解决方式nginx反向代理负载均衡时session共享
第一种、使用ip_hash的轮询策略
一个服务器负责一个用户,但是弊端很大
第二种、使用springSession
将最先前项目中的springSession中配置的一个单独的xml文件拷贝到tomcat9100和tomcat9200的p2p项目WEB-INF/classes下,那个配置文件中封装了所有的springsession配置需要的信息。包括spring session的配置类,spring-data-redis的连接信息等。
这样就实现了集群部署中的session共享了。
同域名下不同项目的session共享
在同一个域名下,有多个不同的项目(项目的上下文根不一样)比如:
这样的话两个同域名下的不同项目会创建两个不同的session。这两个请求cookie的路径不一致,即使存在Spring session共享机制,但是后台服务器会认为这是两个不同的会话,所以不能实现共享。
解决方式
设置Cookie路径为根路径,域名路径
<!-- Spring session 的配置类 -->
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<!--设置cookie的存放方式-->
<property name="cookieSerializer" ref="defaultCookieSerializer"/>
</bean>
<!--设置cookie的存放方式具体实现-->
<bean id="defaultCookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer">
<property name="cookiePath" value="/"/>
</bean>
同根域名不同二级子域名下的项目实现Session共享
修改本地host文件C:\Windows\System32\drivers\etc
使用windows的host映射代理的域名转换实现对网址的访问。
将nginx的ip转换为多个不同地址的域名存放到host文件中
测试
这时候使用本地的idea启动两台tomcat服务器
然后使用浏览器访问
在9200中存放session域的数据
在9100中取出session域中的数据
发现失败
分析Session共享失败的原因
虽然这个两个cookie的路径都设置为了/,但是这两个cookie的域名不一致,虽然加上了Spring Session共享机制,但是服务器认为这是两个不同的会话,所以不能实现共享。
解决方式,设置cookie的域名为根域名 web.com
在applicationContext-session.xml文件中,加如下配置:
注意:域名要和hosts文件中配置的域名后面一样
<!--设置cookie的存放方式具体实现-->
<bean id="defaultCookieSerializer" class="org.springframework.session.web.http.DefaultCookieSerializer">
<property name="cookiePath" value="/"/>
<property name="domainName" value="web.com"/>
</bean>
重新测试,发现可以访问到session中的数据。(注意要清空浏览器和redis数据库的数据)
单点登录
不同根域名下项目实现Session共享,
比如阿里巴巴这样的公司,它存在多个网站,天猫和淘宝都是他们的服务,但是是两个不同的网站。
当用户在淘宝登录之后,打开天猫也会自动登录。这就是session共享的单点登录。
单点登录(Single Sign On),简称为 SSO,是流行的企业业务整合的解决方案之一,SSO是指在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统。
第四章、Spring Session的执行流程(源码分析)
全局过滤器DelegatingFilterProxy
首先所有的页面请求都会被全局的过滤器org.springframework.web.filter.DelegatingFilterProxy过滤,这个过滤器是一个代理过滤器,他不执行真正的过滤逻辑,他代理了一个spring容器中的springSessionRepositoryFilter过滤器。
真正执行过滤逻辑的是SessionRepositoryFilter,其中@Bean注解相当于在xml配置文件中的
<bean id="springSessionRepositoryFilter" class="xx.xxx.xx.SessionRepositoryFilter">
........
</bean>
这个过滤器覆盖了原本servlet中request和response接口中定义操作session的方法,替换成了自己的session方法。
过滤的时候会执行一个finally语句块,在finally语句中提交一下session对象,将自己的这个session对象提交到redis数据库,以hash结构存储。
spring session的配置类
<bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">
<!--设置session过期时间,单位是秒,默认是30分钟-->
<property name="maxInactiveIntervalInSeconds" value="3600"/>
<!--设置cookie的存放方式-->
<property name="cookieSerializer" ref="defaultCookieSerializer"/>
</bean>