147°

分布式架构 共享session的常见解决方案

在使用分布式架构时,会遇到分布式架构常见的几个问题:

分布式事务、接口幂等性、分布式锁和分布式 session。

分布式session

一、什么是session

浏览器在访问一个web服务的时候,会在浏览器中生成一个cookie文件用于在浏览器本地缓存数据,这些数据可以根据浏览器的设置,持久保存在浏览器本地知道手动清除浏览器缓存文件。也可以在结束对一个web服务的会话后(既关闭所有与这个web服务有关的页面和请求)就清空对这个web服务的cookie信息。当浏览器生成cookie时,每次向web服务端发起请求,都会携带一个特殊的jsessionid cookie,根据这个东西,服务端容器(比如tomcat)就会生成一个与之对应的session域,在session域里存放缓存数据。

一般情况下,只要浏览器中的cookie还在,与之对应的session就在。但如果cookie没了,session会根据tomcat配置的生命周期时间,或者web应用的web.xml文件里配置的session生命周期时间而结束。我们一般会使用session来缓存用户的登录信息等,起到一个获取用户登录信息和登录状态检查的作用。

单服务系统中,可以直接拿session来用,但是分布式系统中,session状态怎么维护?

1.完全不用session

JSON WEB TOKEN (缩写 JWT)是目前最流行的跨域认证解决方案,我们来介绍一下它的原理和用法:

一、跨域认证问题

1、用户向服务器发送用户名和密码。

2、服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。

3、服务器向用户返回一个 session_id,写入用户的 Cookie。

4、用户随后的每一次请求,都会通过 Cookie,将 session_id 传回服务器。

5、服务器收到 session_id,找到前期保存的数据,由此得知用户的身份。

这种模式的问题在于,扩展性(scaling)不好。单机当然没有问题,如果是服务器集群,或者是跨域的服务导向架构,就要求 session 数据共享,每台服务器都能够读取 session。

举例来说,A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

一种解决方案是 session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。

另一种方案是服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器。JWT 就是这种方案的一个代表。

二、JWT 的原理

JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

{
  "姓名": "张三",
  "角色": "管理员",
  "到期时间": "2018年7月1日0点0分"
}

以后,用户与服务端通信的时候,都要发回这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展。

既在客户端向服务端发起http请求时,可以在http请求的Header中加入自定义的key:value,自定义的信息包括了参与加密的信息,业务数据和签名信息,然后当服务端收到请求后,根据http请求头中的这些自定义信息,以下是 JWT约定的公共参数:

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

通过密钥等方式,判断请求是否合法,对合法的请求进行处理,不合法的请求直接无视。不再通过session来判断用户合法性和获取用户的信息,实现了跨域系统的访问。

三、JWT 的几个特点

(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。

(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。

(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。

(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

2.tomcat + redis

这个其实还挺方便的,就是使用 session 的代码,跟以前一样,还是基于 tomcat 原生的 session 支持即可,然后就是用一个叫做 Tomcat RedisSessionManager 的东西,让所有我们部署的 tomcat 都将 session 数据存储到 redis 即可

在 tomcat 的配置文件中配置:

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" />
<Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"         host="{redis.host}"         port="{redis.port}"         database="{redis.dbnum}"         maxInactiveInterval="60"/>

然后指定 redis 的 host 和 port 就 ok 了。

<Valve className="com.orangefunction.tomcat.redissessions.RedisSessionHandlerValve" /><Manager className="com.orangefunction.tomcat.redissessions.RedisSessionManager"   sentinelMaster="mymaster"   sentinels="<sentinel1-ip>:26379,<sentinel2-ip>:26379,<sentinel3-ip>:26379"   maxInactiveInterval="60"/>

还可以用上面这种方式基于 redis 哨兵支持的 redis 高可用集群来保存 session 数据,都是 ok 的。

3.spring session + redis

上面所说的第二种方式会与 tomcat 容器重耦合,如果我要将 web 容器迁移成 jetty,难道还要重新把 jetty 都配置一遍?

因为上面那种 tomcat + redis 的方式好用,但是会严重依赖于web容器,不好将代码移植到其他 web 容器上去,尤其是你要是换了技术栈咋整?比如换成了 spring cloud 或者是 spring boot 之类的呢?

所以现在比较好的还是基于 Java 一站式解决方案,也就是 spring。人家 spring 基本上承包了大部分我们需要使用的框架,spirng cloud 做微服务,spring boot 做脚手架,所以用 sping session 是一个很好的选择。

在 pom.xml中,加入spring session和redis的依赖包,配置:​​​​​​​

<dependency>  <groupId>org.springframework.session</groupId>  <artifactId>spring-session-data-redis</artifactId>  <version>1.2.1.RELEASE</version></dependency>
<dependency>  <groupId>redis.clients</groupId>  <artifactId>jedis</artifactId>  <version>2.8.1</version></dependency>

在 spring的配置文件中,配置好redis:​​​​​​​

<bean id="redisHttpSessionConfiguration" class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration">    <property name="maxInactiveIntervalInSeconds" value="600"/></bean>
<bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig">    <property name="maxTotal" value="100" />    <property name="maxIdle" value="10" /></bean>
<bean id="jedisConnectionFactory"      class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" destroy-method="destroy">    <property name="hostName" value="${redis_hostname}"/>    <property name="port" value="${redis_port}"/>    <property name="password" value="${redis_pwd}" />    <property name="timeout" value="3000"/>    <property name="usePool" value="true"/>    <property name="poolConfig" ref="jedisPoolConfig"/></bean>

在 web.xml 中配置:​​​​​​​

<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>

示例代码:​​​​​​​

@RestController@RequestMapping("/test")public class TestController {
    @RequestMapping("/putIntoSession")    public String putIntoSession(HttpServletRequest request, String username) {        request.getSession().setAttribute("name",  "leo");        return "ok";    }
    @RequestMapping("/getFromSession")    public String getFromSession(HttpServletRequest request, Model model){        String name = request.getSession().getAttribute("name");        return name;    }}

上面的代码就是 ok 的,给 sping session 配置基于 redis 来存储 session 数据,然后配置了一个 spring session 的过滤器,这样的话,session 相关操作都会交给 spring session 来管了。接着在代码中,就用原生的 session 操作,就是直接基于 spring sesion 从 redis 中获取数据了。

实现分布式的会话有很多种方式,我说的只不过是比较常见的几种方式,tomcat + redis 早期比较常用,但是会重耦合到 tomcat 中;近些年,通过 spring session 来实现。

本文由【太猪-YJ】发布于开源中国,原文链接:https://my.oschina.net/xiaoyoung/blog/3066406

全部评论: 0

    我有话说: