Steve's Blog

Talk is cheap, show me the code.

0%

如何实现分布式Session-上?

image-20220912000517236

0. 什么是Cookie和Session

“Cookie”这一概念由网景(Netscape)公司的程序员在为客户开发电子商务应用程序时创造1,由于客户不希望总是在服务器中保存事务状态,于是网景提出了Cookies的解决方案。

网景的cookie头字段Set-Cookie,RFC 2965添加了一个Set-Cookie2头字段,即“RFC 2965 cookie”[13][14],但Set-Cookie2很少使用,终于2011年4月的RFC 6265中弃用[15],已经没有现代浏览器可以识别Set-Cookie2头字段[16]

Session代表服务器和客户端之间的一个会话,Session可以记录特定用户的各种数据和配置信息等。

参考这两篇文章12

0.0 为什么要有Cookie和Session?

所有技术的提出都是为了解决遇到的问题,Cookie和Session也不例外。我们常用的HTTP协议有个缺点,就是无状态(stateless)。

于1997年1月提出的RFC2068中提到HTTP/1.1协议是无状态的。

1
2
3
4
5
6
7
8
The Hypertext Transfer Protocol (HTTP) is an application-level
protocol for distributed, collaborative, hypermedia information
systems. It is a generic, stateless, object-oriented protocol which
can be used for many tasks, such as name servers and distributed
object management systems, through extension of its request methods.
A feature of HTTP is the typing and negotiation of data
representation, allowing systems to be built independently of the
data being transferred.

简单来说就是使用HTTP协议进行通信的客户端和服务端,服务端无法判断两次请求是来自同一客户端还是不同客户端,这样的话不同客户的不同网站配置及数据无法正确展示,于是有了Cookie和Session,客户端通过传递Cookie给服务端告诉服务端自己的用户标识信息。

(不过这不是一个好的实现方式,因为可能会有数据安全问题。例如第三方拿到了你的Cookie,用来访问此网站,可能会有数据泄密的风险。所以Cookie这里做了一些安全措施,具体后面会提到。同时,一般不要用Cookie传递敏感个人信息,如密码、隐私如手机号、姓名等,而是通过HTTPS等安全通信协议来传递敏感信息。)

0.1 Cookie和Session存放在哪里?

Cookie保存在客户侧的浏览器中。

查看Google主页请求,可以看到Google服务器返回了3个Set-Cookie字段,包含1P_JARAECNID等字段,这些字段都应该是Google服务器为当前浏览器生成的客户标识信息。

image-20220912001106071

Session存放在Tomcat中,实现类为StandardSession。

Cookie中有个几个固定字段,挨个介绍一下:

  • Cookie声明周期

    • Expires:定义Cookie过期时间,类似的字段还有Max-Age
    • Max-Age:定义Cookie最大有效时间
  • Cookie作用域

    • pathPath 标识指定了主机下的哪些路径可以接受 Cookie(该 URL 路径必须存在于请求 URL 中)。以字符 %x2F (“/“) 作为路径分隔符,子路径也会被匹配。

      例如,设置 Path=/docs,则以下地址都会匹配:

      • /docs
      • /docs/Web/
      • /docs/Web/HTTP
    • DomainDomain 指定了哪些主机可以接受 Cookie。如果不指定,默认为 origin不包含子域名。如果指定了Domain,则一般包含子域名。因此,指定 Domain 比省略它的限制要少。但是,当子域需要共享有关用户的信息时,这可能会有所帮助。

      例如,如果设置 Domain=mozilla.org,则 Cookie 也包含在子域名中(如developer.mozilla.org)。

  • Cookie限制访问

    • Secure:标记为 Secure 的 Cookie 只应通过被 HTTPS 协议加密过的请求发送给服务端,因此可以预防 man-in-the-middle 攻击者的攻击,但这样仍然是不安全的,因为攻击者仍可能通过读取本地硬盘来读取Cookie内容。
    • HttpOnly:JavaScript Document.cookie API 无法访问带有 HttpOnly 属性的 cookie;此类 Cookie 仅作用于服务器。例如,持久化服务器端会话的 Cookie 不需要对 JavaScript 可用,而应具有 HttpOnly 属性。此预防措施有助于缓解跨站点脚本(XSS) (en-US)攻击。

上面是几个常用的字段,如果需要完整的参考,参考这里

1. 为什么要分布式Session?

如果服务是单服务器部署的话,分布式Session是不需要的,如下图。

image-20220916004611701

但是如果是分布式服务,多个tomcat通过nginx服务器做负载均衡,就会出现问题。

  • 请求1通过nginx服务器请求到了tomcat1,会在tomcat1内生成一个session,并将对应的JSESSIONID通过返回到client。
  • 短时间内带着第一次返回的JSESSIONID的另一个请求2,通过nginx服务器可能会请求到另外2个tomcat服务器,而这两个服务器没有存储JSESSIONID对应的session,它们会以为这是个未登录请求,可能会报错或者重新发起登录,那就会出现问题。

image-20220916004236166

解决这个问题有几个思路:

  • 指定应用服务器:通过客户标识(例如uid)做hash,使得每次相同的客户端访问到特定的服务器。这种处理方式有个问题,就是如果其中一个机器挂了,挂在这个机器上的所有用户必须得重新登录。
  • session复制:tomcat自己给定了机制可以实现session复制,通过配置可以实现多个tomcat之间的session复制,但由于大体积session或者session数据频繁变化等问题,导致性能很差,业界使用不广泛,此处不做介绍。 
  • session持久化:将session通过数据库(例如redis、memcached、mysql等,其中redis、memcached由于性能好,用的较多)进行持久化,之后每次访问都从数据库中拿取session信息,这样就可以保证session在集群环境下可以正常使用,但这种处理方式需要注意数据库的单点故障问题。
  • token:token也是为了应对在集群环境下,session扩展性不好的问题。它的思路和其它session处理策略不一样,它的思路是把数据保存到客户端,每次请求是带上来,服务端进行校验,确认没问题后放行。常用的有JWT token,具体可参见阮一峰的这篇博客,此处只做简单介绍。

1.1 JWT token

整个JWT token的格式为:

1
Header.Payload.Signature

例如:

1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbeyJuYW1lIjoiU3RldmUgSm9icyJ9LHsiam9iQ29kZSI6IjAwMDAwMSJ9XSwiaWF0IjoxNjYzNzczMjYyLCJleHAiOjE2NjQ1NTM1OTksImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9.ns4Ko5yrNlWtsv9sUyvX3fwNwYcShQH14wXymmB0FV4

Header和Payload均经过Base64Url算法编码。Base64Url算法相比Base64有一些不同:由于token有时候可能放到url中作为参数传递,Base64算法中有三个字符+, =/,由于在url中有特殊含义,需要替换掉。Base64Url算法会忽略掉=+号替换成-/号替换成_

其中Header及含义为:

1
2
3
4
5
//  密文为eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
{
"alg": "HS256",
"typ": "JWT"
}

Payload为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 密文为eyJkYXRhIjpbeyJuYW1lIjoiU3RldmUgSm9icyJ9LHsiam9iQ29kZSI6IjAwMDAwMSJ9XSwiaWF0IjoxNjYzNzczMjYyLCJleHAiOjE2NjQ1NTM1OTksImF1ZCI6IiIsImlzcyI6IiIsInN1YiI6IiJ9
{
// data为用户自定义数据
"data": [{
"name": "Steve Jobs"
}, {
"jobCode": "000001"
}],
// iat是签发时间戳
"iat": 1663773262,
// exp是本条token过期的时间
"exp": 1664553599,
"aud": "",
"iss": "",
"sub": ""
}

密钥secret为JustATestForJwt

根据Header、Payload和密钥生成的签名(signature)为

1
ns4Ko5yrNlWtsv9sUyvX3fwNwYcShQH14wXymmB0FV4

signature的生成方式为:

1
2
3
4
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)

Jwt的使用方式一般放在Cookie或者localStorage里,请求的时候放入request header中的Authorization字段中(放入Cookie中不能跨域)例如:

1
Authorization: Bearer <token>

或者直接放到post请求body中。

Jwt的问题

  • 不能控制失效时间,签发后在expire date之前一直有效,除非更换secret,但是这样所有之前签发的token都会失效
  • 其他人获取到token后就可以使用,所以为了安全,token有效期应该设置比较短,重要权限应该使用时重新认证。
  • payload默认不加密,所以不要把敏感信息放到payload中。

总之,JWT token适合对于数据安全要求不是特别高、但是适当需要校验的业务场景,如果对数据安全要求比较高,那么不应该使用JWT token,而是RSA之类的加密算法。