跳转至

客户端数据维持的方式

在进行后端编程时,一个绕不开的问题就是如何维持用户客户端的数据(最常见的数据便是登录数据)。

当前的客户端数据维持方式大抵分为:Cookie、Session、Token(以及基于 Token 方式发展的 Jwt 方式)。

Cookie 是服务器发送倒用户浏览器并保存在本地的一小块数据,浏览器保存 Cookie 之后再向服务器发送请求时便会一并携带上 Cookie,从而用于告知服务器该请求来自之前发送 Cookie 给的同一客户端,让无状态的 HTTP 得以保持包括用户的登入状态等的状态信息。

Cookie 曾一度用于客户端数据的唯一存储,因为当时并没有其他合适的存储方法,所以 Cookie 是唯一的存储手段。然而随着更新的存储手段出现,Cookie 仍未被抛弃,因为 Cookie 的客户端发送性质可以降低服务器端的压力,用来存储一些相对不那么重要的信息,如用户习惯。

Cookie 存在安全性问题,因为其浏览器自动发送的特性使其可以很方便地被获取,所以为避免遭受 CRSF 攻击导致 Cookie 盗用等情况,现今已很少使用 Cookie 进行登录验证。

CSRF(Cross-site request forgery)跨站请求伪造:诱导受害者点入一个有着向被攻击网站发送特定请求的虚假网站,使得受害者的浏览器使用受害者的 Cookie 完成该特定请求。

需要注意的是,CSRF 攻击并非直接获取受害人的 Cookie 等信息,而是利用受害人的浏览器发送带有 Cookie 的请求完成攻击,类似于借刀杀人。

Cookie 的大致结构包含:名称、内容、域名、路径、发送原因、脚本可访问性、创建时间、到期时间。

这里需要说明下 Cookie 的域名。

Cookie 的域名也被称作作用域,是带来跨域问题使得 Cookie 不符合现代网络应用需要的原因之一。

Path 标识的是指定主机下的哪些路径可以接受 Cookie

Domain 标识为保证安全,指定了哪些主机可以接受 Cookie。若 Cookie 中不指定 Domain,则默认值为当前文档的主机(不包括主机下的子域名,如当 Domain 的值为 demo.com 时,Cookie 依然无法被 demo.service1.com 获取);若指定了 Domain ,则一般也能包含子域名,如有两个域 domain1.test.comdomain2.test.com ,此时指定 Domain 值为 .test.com,则 cookie 可同时访问到两个域。

综上,一些大网站上使用 Cookie 的场景中常见如下 Cookies :

image-20230126160930782

上图中,Cookie 的域为网站的一二级域名,这使得 Cookie 可以被不同的三级域共享,如:

image-20230126161106395

而对于三级域独享的 Cookies,其域值则为:

image-20230126161149554

当应用包含多个服务时便需要做单点登录,而其中一种常用的方式便是 Cookie + Redis 的 SSO 方式,这个方式当然也可以用 jwt 等进行登录,然后使用 Cookie + Redis 来维持状态从而减少服务端的计算量,但相比单纯的 jwt 当然会存在安全风险。

此方式中一般需要设立一个登录服务,并在登录后生成一个随机值作为用户数据在 redis 中存储的 key 值,而将用户数据存入 value,同时颁发指定了方便拓展的域值的 Cookie,在其中存储方才 redis 的用户 key 值。

此时所有服务均可在获取 Cookie 后提取 key 值,到 redis 根据 key 值进行查询,若查询到数据则为登录模式。

Session 方式

除了可以将用户信息通过 Cookie 存储在用户浏览器中,也可以利用 Session 将信息存储在服务端,由此便为早期的 Cookie + Session 用户状态维持方案:

  1. 用户登录
  2. 服务器验证用户名和密码,正确则将信息放入 Session,信息的 key 值记为 Session ID
  3. 在服务器响应报文中的首部字段 Set-Cookie 中包含此 Session ID,使得客户端收到响应报文后将该 Cookie 存入浏览器中
  4. 之后用户访问服务端时便会携带该 Cookie,服务端在收到请求后根据 Cookie 中的 Session ID 去用存储在 Session 中的信息进行操作

可以看到,Session 方式本质上和 Redis 并无太大差别,但是 Session 存在跨域问题:多个服务端不会共享同一个 Session,因此若要使用此方式进行现代网页应用程序的用户信息维持方案,必须进行 Session 广播,也即将 Session 复制到所有服务中去。很明显地,这会带来额外的开销。

因此 Session 方式可以说被时代淘汰了。

token方式

token 是一个存储着用户登录信息的字符串,具有天生防止 CRSF 攻击的特性,后端收到请求后对用户登录信息进行加密,并生成一个 token 将信息保存至其中后交给前端,同时在数据库中保存该 token,前端再将 token 转发给浏览器,由浏览器保存 token。

转发 token 时应将其放在响应头中,使用浏览器的 local storage 或 Cookie 进行存储,再次访问时再由前端将其取出后置入请求头使用(如根据 token 动态生成页面,或将其转发给后端),从而使得攻击者无法通过 CRSF 攻击来伪装成受害人进行不安全的操作,此时前端的代码可以如下:

// 设置拦截器
service.interceptors.request.use((config) => {
  if (cookie.get('token')) {
    // 客户持有名为token的cookie时将其取出放入请求头
    config.headers['token'] = cookie.get('token')
  }
  return config
})

而当后端收到 token 后会查询数据库验证 token 是否有效,这使得此方式具有中心化特性,需依赖内存或 redis 存储,且在分布式系统中需要 redis 查询和接口调用从而增加系统复杂性。

jwt (Json web token)

Jwt 方式本质上也是一种 token 方式,只是为避免额外的数据库查询开销,为 token 多加一个 payload (载荷),将用户数据存入 payload 并对整个 jwt 进行签名和加密;由此,服务端只需对 token 的签名进行解密校验即可使用存入其中的用户信息进行后续的业务,从而省略了数据库的查询。

这种方式会导致请求头复杂度的增加、服务端无法自行删除 token 以及可能的密钥泄露风险;但是其无需查询数据库的特性使得其针对分布式应用时具有去中心化的特性,增加应用性能,以及为第三方业务服务提供用户信息时,业务服务只需使用公钥验证 jwt 的真实性便可使用其中提供的内容。

因此,也有一部分公司并不单独使用 jwt 作为访问凭证,而是将其作为可靠的发布信息使用,而自身进行访问认证时则单独设置额外的方式。