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:UserssharexieAppDataLocalGoogleChromeUser DataDefaultCache

  Firefox下的cookie放在:C:UserssharexieAppDataRoamingMozillaFirefoxProfiles q2hit6m.defaultcookies.sqlite (倒数第二个文件名是随机的文件名字)

  Ie下的cookie放在:C:UsersAdministratorAppDataRoamingMicrosoftWindowsCookies

  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端程序调用 HttpServletRequest.getSession(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 :" + session.getId() + "在Session中存入成功!";
    }

    @RequestMapping("/getSession")
    public String getSession(HttpSession session) {
        return "当前项目端口:" + projectPort + " 当前sessionId :" + session.getId() + "  获取的姓名:" + session.getAttribute("name");
    }

}

  我们直接通过轮询机制来访问首先向Session中存入一个姓名,http://www.hello.com/createSession?name=AAA

当前项目端口:8081 当前sessionId :0F20F73170AE6780B1EC06D9B06210DB在Session中存入成功!

  因为我们使用的是默认的轮询机制,那么下次肯定访问的是8080端口,我们直接获取以下刚才存入的值http://www.hello.com/getSession

当前项目端口:8080 当前sessionId :C6663EA93572FB8DAE27736A553EAB89 获取的姓名:null

  发现8080端口中并没有我们存入的值,并且sessionId也是与8081端口中的不同。

  因为轮询机制这个时候我们是8081端口的服务器,那么之前我们是在8081中存入了一个姓名,8080端口的服务端并没有存入Session。

  那么我们再次访问8081端口服务看看是否能够获取到我们存入的姓名:AAA,继续访问:http://www.hello.com/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.com),利用浏览器访问该根域名下的所有二级域名站点时,会传递与之域名对应的所有Cookie内容的特性,从而实现用户的Cookie化Session在多服务间的共享访问。

  这个方案的优点无需额外的服务器资源;缺点是由于受http协议头信息长度的限制,仅能够存储小部分的用户信息,同时Cookie化的 Session内容需要进行安全加解密(如:采用DES、RSA等进行明文加解密;再由MD5、SHA-1等算法进行防伪认证),另外它也会占用一定的带宽资源,因为浏览器会在请求当前域名下任何资源时将本地Cookie附加在http头中传递到服务器,最重要的是存在安全隐患

  2、使用Nginx中的ip绑定策略

  这个只需要在Nginx中简单配置一句 ip_hash; 就可以了,但是该方式的缺点也很明显,配置了IP绑定就不支持Nginx的负载均衡了。具体可以参考博客:https://www.cnblogs.com/ywb-articles/p/10686673.html

  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实现方式,流程大致一致。具体可参考:https://www.cnblogs.com/lightzone/p/9749076.html

  浏览器第一次访问服务器,根据传过来的唯一标识userId,服务端会通过一些算法,如常用的HMAC-SHA256算法,然后加一个密钥,生成一个token,然后通过BASE64编码一下之后将这个token发送给客户端;客户端将token保存起来,下次请求时,带着token,服务器收到请求后,然后会用相同的算法和密钥去验证token,如果通过,执行业务操作,不通过,返回不通过信息。

  优点:

  • 无状态、可扩展 :在客户端存储的Token是无状态的,并且能够被扩展。基于这种无状态和不存储Session信息,负载均衡器能够将用户信息从一个服务传到其他服务器上。
  • 安全:请求中发送token而不再是发送cookie能够防止CSRF(跨站请求伪造)。
  • 可提供接口给第三方服务:使用token时,可以提供可选的权限给第三方应用程序。
  • 多平台跨域

  对应用程序和服务进行扩展的时候,需要介入各种各种的设备和应用程序。 假如我们的后端api服务器a.com只提供数据,而静态资源则存放在cdn 服务器b.com上。当我们从a.com请求b.com下面的资源时,由于触发浏览器的同源策略限制而被阻止。

  我们通过CORS(跨域资源共享)标准和token来解决资源共享和安全问题。

  举个例子,我们可以设置b.com的响应首部字段为:

Access-Control-Allow-Origin: http://a.com

Access-Control-Allow-Headers: Authorization, X-Requested-With, Content-Type, Accept

Access-Control-Allow-Methods: GET, POST, PUT,DELETE

  第一行指定了允许访问该资源的外域 URI。

  第二行指明了实际请求中允许携带的首部字段,这里加入了Authorization,用来存放token。

  第三行用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。

  然后用户从a.com携带有一个通过了验证的token访问B域名,数据和资源就能够在任何域上被请求到。

  5、使用tomcat内置的session同步

  5.1 tomcat中server.xml直接配置

<!-- 第1步:修改server.xml,在Host节点下添加如下Cluster节点 -->
<!-- 用于Session复制 -->
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster" channelSendOptions="8">
    <Manager className="org.apache.catalina.ha.session.DeltaManager" expireSessionsOnShutdown="false" notifyListenersOnReplication="true" />
    <Channel className="org.apache.catalina.tribes.group.GroupChannel">
        <Membership className="org.apache.catalina.tribes.membership.McastService" address="228.0.0.4" 
                    port="45564" frequency="500" dropTime="3000" />
        <!-- 这里如果启动出现异常,则可以尝试把address中的"auto"改为"localhost" -->
        <Receiver className="org.apache.catalina.tribes.transport.nio.NioReceiver" address="auto" port="4000" 
                  autoBind="100" selectorTimeout="5000" maxThreads="6" />
        <Sender className="org.apache.catalina.tribes.transport.ReplicationTransmitter">
            <Transport className="org.apache.catalina.tribes.transport.nio.PooledParallelSender" />
        </Sender>
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.TcpFailureDetector" />
        <Interceptor className="org.apache.catalina.tribes.group.interceptors.MessageDispatchInterceptor" />
    </Channel>
    <Valve className="org.apache.catalina.ha.tcp.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 修改web.xml

  web.xml中需加入<distributable/> 以支持集群。

<distributable/>

  5、Spring-Session+Redis实现

  Spring提供了一个解决方案:Spring-Session用来解决两个服务之间Session共享的问题。

  5.1 在pom.xml中添加相关依赖

<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.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
</dependency>

  5.2 修改application.properties全局配置文件(本地要开启redis服务)

spring.redis.database=0
spring.redis.host=localhost
spring.redis.port=6379
#spring.redis.password=
spring.redis.jedis.pool.max-active=8
spring.redis.jedis.pool.max-wait=-1
spring.redis.jedis.pool.max-idle=8
spring.redis.jedis.pool.min-idle=8
spring.redis.timeout=10000

  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
    public StringRedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        return new StringRedisTemplate(redisConnectionFactory);
    }
}

  5.4 初始化Session配置

/**
 * 初始化Session配置
 */
public class RedisSessionInitializer extends AbstractHttpSessionApplicationInitializer {
    public RedisSessionInitializer() {
        super(RedisSessionConfig.class);
    }
}

  Spring-Sesion实现的原理

  当Web服务器接收到请求后,请求会进入对应的Filter进行过滤,将原本需要由Web服务器创建会话的过程转交给Spring-Session进行创建Spring-Session会将原本应该保存在Web服务器内存的Session存放到Redis中。然后Web服务器之间通过连接Redis来共享数据,达到Sesson共享的目的。

原文地址:https://www.cnblogs.com/jing99/p/11785070.html