Session机制详解及分布式中Session共享解决⽅案
引⽤⽹址:
⼀、为什么要产⽣Session
http协议本⾝是⽆状态的,客户端只需要向服务器请求下载内容,客户端和服务器都不记录彼此的历史信息,每⼀次请求都是独⽴的。
为什么是⽆状态的呢?因为浏览器与服务器是使⽤socke套接字进⾏通信,服务器将请求结果返回给浏览器之后,会关闭当前的socket 链接,⽽且服务器也会在处理页⾯完毕之后销毁页⾯对象。
然⽽在Web应⽤的很多场景下需要维护⽤户状态才能正常⼯作(是否登录等),或者说提供便捷(记住密码,浏览历史等),状态的保持就是⼀个很重要的功能。因此在web应⽤开发⾥就出现了保持http链接状态的技术:⼀个是cookie技术,另⼀种是session技术。
⼆、Session有什么作⽤,如何产⽣并发挥作⽤
要明⽩Session就必须要弄明⽩什么是Cookie,以及Cookie和Session的关系。
1、什么是Cookie
Cookie技术是http状态保持在客户端的解决⽅案,Cookie就是由服务器发给客户端的特殊信息,⽽这些信息以⽂本⽂件的⽅式存放在客户端,然后客户端每次向服务器发送请求的时候都会带上这些特殊的信息。
2、Cookie的产⽣
当⽤户⾸次使⽤浏览器访问⼀个⽀持Cookie的⽹站的时候,⽤户会提供包括⽤户名在内的个⼈信息并且提交⾄服务器;接着,服务器在向客户端回传相应的超⽂本的同时也会发回这些个⼈信息,当然这些信息并不是存放在HTTP响应体(Response Body)中的,⽽是存放于HTTP响应头(Response Header);当客户端浏览器接收到来⾃服务器的响应之后,浏览器会将这些信息存放在⼀个统⼀的位置。
存储在硬盘上的cookie 不可以在不同的浏览器间共享,可以在同⼀浏览器的不同进程间共享,⽐如两个IE窗⼝。这是因为每中浏览器存储cookie的位置不⼀样,⽐如
Chrome下的cookie放在:C:\Users\sharexie\AppData\Local\Google\Chrome\User Data\Default\Cache
Firefox下的cookie放在:C:\Users\sharexie\AppData\Roaming\Mozilla\Firefox\Profiles\tq2hit6m.defaul
t\cookies.sqlite (倒数第⼆个⽂件名是随机的⽂件名字)
Ie下的cookie放在:C:\Users\Administrator\AppData\Roaming\Microsoft\Windows\Cookies
3、Cookie的内容、作⽤域以及有效期
cookie的内容主要包括:名字,值,过期时间,路径和域。路径与域合在⼀起就构成了cookie的作⽤范围。
如果不设置过期时间,则表⽰这个cookie的⽣命期为浏览器会话期间,只要关闭浏览器窗⼝,cookie就消失了,这种⽣命期为浏览器会话期的 cookie被称为会话cookie。会话cookie⼀般不存储在硬盘上⽽是保存在内存⾥。如果设置了过期时间,浏览器就会把cookie保存到硬盘上,关闭后再次打开浏览器,这些cookie仍然有效直到超过设定的过期时间。
4、Cookie如何使⽤
cookie 的使⽤是由浏览器按照⼀定的原则在后台⾃动发送给服务器的。
当客户端⼆次向服务器发送请求的时候,浏览器检查所有存储的cookie,如果某个cookie所声明的作⽤范围⼤于等于将要请求的资源所在的位置,则把该cookie附在请求资源的HTTP请求头上发送给服务器。
有了Cookie这样的技术实现,服务器在接收到来⾃客户端浏览器的请求之后,就能够通过分析存放于请求头的Cookie得到客户端特有的信息,从⽽动态⽣成与该客户端相对应的内容。通常,我们可以从很多⽹站的登录界⾯中看到“请记住我”这样的选项,如果你勾选了它之后再登录,那么在下⼀次访问该⽹站的时候就不需要进⾏重复⽽繁琐的登录动作了,⽽这个功能就是通过Cookie实现的。
5、什么是Session
Session⼀般叫做回话,Session技术是http状态保持在服务端的解决⽅案,它是通过服务器来保持状态的。我们可以把客户端浏览器与服务器之间⼀系列交互的动作称为⼀个 Session。是服务器端为客户端所开辟的存储空间,在其中保存的信息就是⽤于保持状态。因
此,session是解决http协议⽆状态问题的服务端解决⽅案,它能让客户端和服务端⼀系列交互动作变成⼀个完整的事务。
6、Session的创建
那么Session在何时创建呢?当然还是在服务器端程序运⾏的过程中创建的,不同语⾔实现的应⽤程序有不同创建Session的⽅法。
当客户端第⼀次请求服务端,当server端程序调⽤ Session(true)这样的语句时
的时候,服务器会为客户端创建⼀个session,并将通过特殊算法算出⼀个session的ID,⽤来标识该session对象。
Session存储在服务器的内存中(tomcat服务器通过StandardManager类将session存储在内存中),也可以持久化到file,数据
库,memcache,redis等。客户端只保存sessionid到cookie中,⽽不会保存session。
浏览器的关闭并不会导致Session的删除,只有当超时、程序调⽤HttpSession.invalidate()以及服务端程序关闭才会删除。
7、Tomcat中的Session创建
ManagerBase是所有session管理⼯具类的基类,它是⼀个抽象类,所有具体实现session管理功能的类都要继承这个类,该类有⼀个受保护的⽅法,该⽅法就是创建sessionId值的⽅法:
(tomcat的session的id值⽣成的机制是⼀个随机数加时间加上jvm的id值,jvm的id值会根据服务器的硬件信息计算得来,因此不同jvm的id值都是唯⼀的)。
StandardManager类是tomcat容器⾥默认的session管理实现类,它会将session的信息存储到web容器所在服务器的内存⾥。
PersistentManagerBase也是继承ManagerBase类,它是所有持久化存储session信息的基类,PersistentManager继承了PersistentManagerBase,但是这个类只是多了⼀个静态变量和⼀个getName⽅法,⽬前看来意义不⼤,对于持久化存储session,tomcat还提供了StoreBase的抽象类,它是所有持久化存储session的基类,另外tomcat还给出了⽂件存储FileStore和数据存储JDBCStore两个实现。
8、Cookie与Session的关系
cookie和session的⽅案虽然分别属于客户端和服务端,但是服务端的session的实现对客户端的cookie有依赖关系的,服务端执⾏session机制时候会⽣成session的id值,这个id值会发送给客户端,客户端每次请求都会把这个id值放到http请求的头部发送给服务端,⽽这个id值在客户端会保存下来,保存的容器就是cookie,因此当我们完全禁掉浏览器的cookie的时候,服务端的session也会不能正常使⽤。三、分布式系统中Session共享问题
其实就是服务器集Session共享的问题,在客户端与服务器通讯会话保持过程中,Session记录整个通讯的会话基本信息。但是在集环境中,假设客户端第⼀次访问服务A,服务A响应返回了⼀个sessionId并且存⼊了本地Cookie中。第⼆次不访问服务A了,转去访问服务B。因为客户端中的Cookie中已经存有了sessionId,所以访问服务B的时候,会将sessionId加⼊到请求头中,⽽服务B因为通过sessionId没有到相对应的数据,因此它就会创建⼀个新的sessionId并且响应返回给客户端。这样就造成了不能共享Session的问题。
例如在SpringCloud项⽬中,启动⼀个服务,分别⽤两个不同的端⼝,然后在Eureka Server中注册,那么这样就形成了两台服务的集,Ribbon的负载均衡策略设置为轮询策略。如服务端处理请求为:
@RestController
public class TestSessionController {
@Value("${server.port}")
private Integer projectPort;// 项⽬端⼝
@RequestMapping("/createSession")
public String createSession(HttpSession session, String name) {
session.setAttribute("name", name);
return "当前项⽬端⼝:" + projectPort + " 当前sessionId :" + Id() + "在Session中存⼊成功!";
}
@RequestMapping("/getSession")
public String getSession(HttpSession session) {
return "当前项⽬端⼝:" + projectPort + " 当前sessionId :" + Id() + " 获取的姓名:" + Attribute("name");
}
}
我们直接通过轮询机制来访问⾸先向Session中存⼊⼀个姓名,www.hello/createSession?name=AAA
当前项⽬端⼝:8081 当前sessionId :0F20F73170AE6780B1EC06D9B06210DB在Session中存⼊成功!
因为我们使⽤的是默认的轮询机制,那么下次肯定访问的是8080端⼝,我们直接获取以下刚才存⼊的值
www.hello/getSession
当前项⽬端⼝:8080 当前sessionId :C6663EA93572FB8DAE27736A553EAB89 获取的姓名:null
发现8080端⼝中并没有我们存⼊的值,并且sessionId也是与8081端⼝中的不同。
因为轮询机制这个时候我们是8081端⼝的服务器,那么之前我们是在8081中存⼊了⼀个姓名,8080端⼝的服务端并没有存⼊Session。
那么我们再次访问8081端⼝服务看看是否能够获取到我们存⼊的姓名:AAA,继续访问:www.hello/getSession
当前项⽬端⼝:8081 当前sessionId :005EE6198C30D7CD32FBD8B073531347 获取的姓名:null
发现不但8080端⼝没有,连之前存⼊过的8081端⼝也没有了。
其实发现第三次访问8081的端⼝sessionid都不⼀样了,是因为我们在第⼆次去访问的时候访问的是8080端⼝这个时候客户端在cookie 中获取8081的端⼝去8080服务器上去,没有到后重新创建了⼀个session并且将sessionid响应给客户端,客户端⼜保持到cookid中替换了之前8081的sessionid,那么第三次访问的时候拿着第⼆次访问的sessionid去⼜不到然后⼜创建。⼀直反复循环,两个服务器永远拿
到的是对⽅⽣成的sessionId,拿不到⾃⼰⽣成的sessionId,这就是集中Session共享问题。
四、如何解决Session共享问题
常见Session共享⽅案有如下⼏种:
使⽤cookie来完成(很明显这种不安全的操作并不可靠)
使⽤Nginx中的ip绑定策略,同⼀个ip只能在指定的同⼀个机器访问(不⽀持负载均衡)
使⽤数据库同步session(效率不⾼)
使⽤tomcat内置的session同步(同步可能会产⽣延迟)
使⽤token代替session
负载均衡器的作用使⽤Spring-Session+Redis实现
1、使⽤Cookie实现
这个⽅式原理是将系统⽤户的Session信息加密、序列化后,以Cookie的⽅式,统⼀种植在根域名下(如:.host),利⽤浏览器访问该根域名下的所有⼆级域名站点时,会传递与之域名对应的所有Cookie内容的特性,从⽽实现⽤户的Cookie化Session在多服务间的共享访问。
这个⽅案的优点⽆需额外的服务器资源;缺点是由于受http协议头信息长度的限制,仅能够存储⼩部分的⽤户信息,同时Cookie化的Session内容需要进⾏安全加解密(如:采⽤DES、RSA等进⾏明⽂加解密;再由MD5、SHA-1等算法进⾏防伪认证),另外它也会占⽤⼀定的带宽资源,因为浏览器会在请求当前域名下任何资源时将本地Cookie附加在http头中传递到服务器,最重要的是存在安全隐患。
2、使⽤Nginx中的ip绑定策略
这个只需要在Nginx中简单配置⼀句 ip_hash; 就可以了,但是该⽅式的缺点也很明显,配置了IP绑定就不⽀持Nginx的负载均衡了。具体可以参考博客:
3、使⽤数据库同步session
以为MySQL为例,每次将session数据存到数据库中。这个⽅案还是⽐较可⾏的,不少开发者使⽤了这种⽅式。但它的缺点在于Session的并发读写能⼒取决于MySQL数据库的性能,对数据库的压⼒⼤,同时需要⾃⼰实现Session淘汰逻辑,以便定时从数据表中更新、删除 Session记录,当并发过⾼时容易出现表锁,虽然可以选择⾏级锁的表引擎,但很多时候这个⽅案不是最优⽅案。
4、使⽤token代替session
这⾥token是JSON Web Token,⼀般⽤它来替换掉Session实现数据共享。
使⽤基于 Token 的⾝份验证⽅法,在服务端不需要存储⽤户的登录记录。⼤概的流程是这样的:
1、客户端通过⽤户名和密码登录服务器;
2、服务端对客户端⾝份进⾏验证;
3、服务端对该⽤户⽣成Token,返回给客户端;
4、客户端将Token保存到本地浏览器,⼀般保存到cookie中;
5、客户端发起请求,需要携带该Token;
6、服务端收到请求后,⾸先验证Token,之后返回数据。
如上图,左图为Token实现⽅式,有图为session实现⽅式,流程⼤致⼀致。具体可参考:
浏览器第⼀次访问服务器,根据传过来的唯⼀标识userId,服务端会通过⼀些算法,如常⽤的HMAC-SHA256算法,然后加⼀个密钥,⽣成⼀个token,然后通过BASE64编码⼀下之后将这个token发送给客户端;客户端将token保存起来,下次请求时,带着token,服务器收到请求后,然后会⽤相同的算法和密钥去验证token,如果通过,执⾏业务操作,不通过,返回不通过信息。
优点:
⽆状态、可扩展:在客户端存储的Token是⽆状态的,并且能够被扩展。基于这种⽆状态和不存储Session信息,负载均衡器能够将⽤户信息从⼀个服务传到其他服务器上。
安全:请求中发送token⽽不再是发送cookie能够防⽌CSRF(跨站请求伪造)。
可提供接⼝给第三⽅服务:使⽤token时,可以提供可选的权限给第三⽅应⽤程序。
多平台跨域
对应⽤程序和服务进⾏扩展的时候,需要介⼊各种各种的设备和应⽤程序。假如我们的后端api服务器a只提供数据,⽽静态资源则存放在cdn 服务器b上。当我们从a请求b下⾯的资源时,由于触发浏览器的同源策略限制⽽被阻⽌。
我们通过CORS(跨域资源共享)标准和token来解决资源共享和安全问题。
举个例⼦,我们可以设置b的响应⾸部字段为:
Access-Control-Allow-Origin: a
Access-Control-Allow-Headers: Authorization, X-Requested-With, Content-Type, Accept
Access-Control-Allow-Methods: GET, POST, PUT,DELETE
第⼀⾏指定了允许访问该资源的外域 URI。
第⼆⾏指明了实际请求中允许携带的⾸部字段,这⾥加⼊了Authorization,⽤来存放token。
第三⾏⽤于预检请求的响应。其指明了实际请求所允许使⽤的 HTTP ⽅法。
然后⽤户从a携带有⼀个通过了验证的token访问B域名,数据和资源就能够在任何域上被请求到。
5、使⽤tomcat内置的session同步
5.1 tomcat中l直接配置
<!-- 第1步:修改l,在Host节点下添加如下Cluster节点 -->
<!-- ⽤于Session复制 -->
<Cluster className="org.apache.p.SimpleTcpCluster" channelSendOptions="8">
<Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true" />
<Channel className="org.up.GroupChannel">
<Membership className="org.bership.McastService" address="228.0.0.4"
port="45564" frequency="500" dropTime="3000" />
<!-- 这⾥如果启动出现异常,则可以尝试把address中的"auto"改为"localhost" -->
<Receiver className="org.ansport.nio.NioReceiver" address="auto" port="4000"
autoBind="100" selectorTimeout="5000" maxThreads="6" />
<Sender className="org.ansport.ReplicationTransmitter">
<Transport className="org.ansport.nio.PooledParallelSender" />
</Sender>
<Interceptor className="org.up.interceptors.TcpFailureDetector" />
<Interceptor className="org.up.interceptors.MessageDispatchInterceptor" />
</Channel>
<Valve className="org.apache.p.ReplicationValve" filter="" />
<Valve className="org.apache.catalina.ha.session.JvmRouteBinderValve" />
<Deployer className="org.apache.catalina.ha.deploy.FarmWarDeployer" tempDir="/tmp/war-temp/"
deployDir="/tmp/war-deploy/" watchDir="/tmp/war-listen/" watchEnabled="false" />
<ClusterListener className="org.apache.catalina.ha.session.ClusterSessionListener" />
</Cluster>
5.2 修改l
l中需加⼊<distributable/> 以⽀持集。
<distributable/>
5、Spring-Session+Redis实现
Spring提供了⼀个解决⽅案:Spring-Session⽤来解决两个服务之间Session共享的问题。
5.1 在l中添加相关依赖
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- spring session 与redis应⽤基本环境配置,需要开启redis后才可以使⽤,不然启动Spring boot会报错 -->
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.apachemons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
5.2 修改application.properties全局配置⽂件(本地要开启redis服务)
#dis.password=
5.3 在代码中添加Session配置类
/**
* 这个类⽤配置redis服务器的连接
* maxInactiveIntervalInSeconds为SpringSession的过期时间(单位:秒)
*/
@EnableRedisHttpSession(maxInactiveIntervalInSeconds = 1800)
public class SessionConfig {
// 冒号后的值为没有配置⽂件时,制动装载的默认值
@Value("${redis.hostname:localhost}")
private String hostName;
@Value("${redis.port:6379}")
private int port;
// @Value("${redis.password}")
// private String password;
@Bean
public JedisConnectionFactory jedisConnectionFactory() {
RedisStandaloneConfiguration redisStandaloneConfiguration =
new RedisStandaloneConfiguration();
redisStandaloneConfiguration.setHostName(hostName);
redisStandaloneConfiguration.setPort(port);
// redisStandaloneConfiguration.setDatabase(0);
// redisStandaloneConfiguration.setPassword(RedisPassword.of("123456"));
return new JedisConnectionFactory(redisStandaloneConfiguration);
}
@Bean
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论