JWT、Session

参考

简述

基本所有网站都有登录功能,登录之后再次请求依然是登录状态。但 http 是无状态的,也就是请求和请求之间没有关联,但我们很多功能的实现是需要保存状态的。

给 http 添加状态有两种方式:

  • 服务端存储的 session + cookie 的方案
  • 客户端存储的 jwt token 的方案

但这两种方式也都有各自的缺点。也有一些解决方案。

常用的方案基本是 **<u><font style="color:rgb(37, 41, 51);">session + redis</font></u>****<u><font style="color:rgb(37, 41, 51);">jwt + redis</font></u>** 两种

session + cookie 给 http 添加状态的方案是服务端保存 session 数据,然后把 id 放入 cookie 返回,cookie 是自动携带的,每个请求可以通过 cookie 里的 id 查找到服务器对应的 session,从而实现请求的标识。

问题 1:CSRF

CSRF(跨站请求伪造)即因为 cookie 会在请求时自动带上,那你在一个网站登录了,再访问别的网站,万一里面有个按钮会请求之前那个网站的,那 cookie 依然能带上。这就跳过登录,用户数据就寄了。

解决方案

  1. 一般会验证 referer,就是请求是哪个网站发起的,如果发起请求的网站不对,那就阻止掉。但 referer 也有伪造风险。
  2. 但可以用随机值 token 来解决,每次随机生成一个值返回,后面再发起的请求需要包含这个值才行,否则就认为是非法的。

问题 2:分布式 session

session 是把状态数据保存在服务端,万一并发量大了,需要服务器时,那不同服务器之间的 session 怎么同步了?登录之后 session 是保存在某一台服务器的,之后可能会访问到别的服务器,这时候那台服务器是没有对应的 session 的,就没法完成对应的功能啦。

解决方案

  1. 一种是 session 复制,也就是通过一种机制在各台机器自动复制 session,并且每次修改都同步下。这个有对应的框架来做,比如 java 的 spring-session。
  2. 还有一种方案是把 session 保存在 redis,这样每台服务器都去那里查,只要一台服务器登录了,其他的服务器也就能查到 session,这样就不需要复制了。(常用)

问题 3:跨域

cookie 为了安全,是做了 domain 的限制的,设置 cookie 的时候会指定一个 domain,只有这个 domain 的请求才会带上这个 cookie。不同 domain 的请求没法带上 cookie。

解决方案

  1. 俩个域名统一成同一个,服务端做下中转
  2. ajax 请求跨域时不会挟带 cookie 需手动设置 withCredentials 为 true,后端也要做相应调整:
1
2
3
// allow origin 设置 * 都不行,必须指定具体的域名才能接收跨域
Access-Control-Allow-Origin: "当前域名";
Access-Control-Allow-Credentials: true

客户端存储的 token

JWT 的方案是把状态数据保存在 header 里,每次请求需要手动携带,没有 session + cookie 方案的 CSRF、分布式、跨域的问题,业务中很常用。但是也有安全性、性能、没法控制等问题。

token 的方案常用 json 格式来保存,叫做 json web token,简称 **<font style="color:rgb(37, 41, 51);">JWT</font>**

如图 JWT 是由 header、payload、verify signature 三部分组成的:

<font style="color:rgb(37, 41, 51);">header</font> 部分保存当前的加密算法,<font style="color:rgb(37, 41, 51);">payload</font> 部分是具体存储的数据,<font style="color:rgb(37, 41, 51);">verify signature</font> 部分是把 header 和 payload 还有 salt 做一次加密之后生成的。(salt,盐,就是一段任意的字符串,增加随机性)。这三部分会分别做 Base64,然后连在一起就是 JWT 的 header,放到某个 header 比如 authorization 中:

1
authorization: Bearer xxxxx.xxxxx.xxxx

请求的时候把这个 header 带上,服务端就可以解析出对应的 header、payload、verify signature 这三部分,然后根据 header 里的算法也对 header、payload 加上 salt 做一次加密,如果得出的结果和 verify signature 一样,就接受这个 token。

把状态数据都保存在 payload 部分,这样就实现了有状态的 http:

  • **<font style="color:rgb(37, 41, 51);">CSRF</font>**:因为不是通过自动带的 cookie 来关联服务端的 session 保存的状态,所以没有 CSRF 问题。
  • **<font style="color:rgb(37, 41, 51);">分布式 session</font>**: 因为状态不是保存在服务端,所以无论访问哪台服务器都行,只要能从 token 里解析出状态数据就行。
  • **<font style="color:rgb(37, 41, 51);">跨域</font>**:因为不是 cookie 那一套,自然也没有跨域的限制,只要手动带上 JWT 的 header 就行。

安全性问题

因为 JWT 把数据直接 Base64 之后就放在了 header 里,那别人就可以轻易从中拿到状态数据,比如用户名等敏感信息,也能根据这个 JWT 去伪造请求。

所以 JWT 要搭配 https 来用,让别人拿不到 header。

性能问题

JWT 把状态数据都保存在了 header 里,每次请求都会带上,比起只保存个 id 的 cookie 来说,请求的内容变多了,性能也会差一些。所以 JWT 里也不要保存太多数据。

没法让 JWT 失效

因为是保存在客户端,没法手动让他失效的。比如踢人、退出登录、改完密码下线这种功能就没法实现。

但也可以配合 redis 来解决,记录下每个 token 对应的生效状态,每次先去 redis 查下 jwt 是否是可用的,这样就可以让 jwt 失效。


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!