Ambas formas de agregar estado a HTTP no son perfectas

Este artículo participa en la convocatoria de temas técnicos para el camino avanzado de Node.js, haga clic para ver los detalles "

Sabemos que http no tiene estado, lo que significa que no hay correlación entre la solicitud anterior y la siguiente. Sin embargo, si queremos implementar las funciones de la aplicación, a menudo necesitamos tener estado, por ejemplo, después de iniciar sesión y luego agregar al carrito de compras, se debe reconocer que el usuario que inició sesión lo hizo.

¿Cómo agregar estado a la solicitud http?

Hay dos soluciones a este problema: la solución de sesión + cookie almacenada en el lado del servidor y la solución de token almacenado en el lado del cliente.

Pero, de hecho, estos dos esquemas no son muy buenos y no son perfectos.

¿Por qué dices eso? Veámoslos por separado:

Sesión + cookie almacenada en el servidor

Para agregar un estado a http, marque cada solicitud con una marca y luego almacene los datos correspondientes a esta marca en el servidor. De esta manera, cada solicitud marcada puede encontrar los datos correspondientes, y es natural almacenar el estado, como el inicio de sesión y los permisos.

Esta etiqueta debe transportarse automáticamente, por lo que http ha diseñado un mecanismo de cookies y los datos almacenados en él se transportarán con cada solicitud.

Luego, de acuerdo con la etiqueta en la cookie, los datos correspondientes en el lado del servidor se denominan sesión, y esta etiqueta es la identificación de la sesión.

Como se muestra en la figura, debido a que la solicitud trae automáticamente una cookie, la sesión correspondiente a la identificación de 1 se puede encontrar para ambas solicitudes, y es natural saber quién es el usuario conectado actualmente, y también puede almacenar otro estado datos.

Este es el esquema de sesión + cookie para agregar estado a http.

¿Crees que hay un problema con este plan?

Hay problemas, y hay bastantes.

Uno de los mayores problemas es el infame CSRF (Cross Site Request Forgery):

CSRF

Debido a que la cookie se transportará automáticamente cuando se realice la solicitud, entonces, si inicia sesión en un sitio web y luego visita otro sitio web, si hay un botón que solicitará el sitio web anterior, la cookie aún se puede transportar. Y entonces ya no tienes que iniciar sesión.

Entonces, ¿qué pasa si se realizan algunas operaciones peligrosas después de presionar este botón?

Es peligroso.

Y, en general, los sitios web que usan vulnerabilidades CSRF están bien disfrazados, lo que dificulta que usted vea las fallas.Dichos sitios web se denominan sitios web de phishing.

为了解决这个问题,我们一般会验证 referer,就是请求是哪个网站发起的,如果发起请求的网站不对,那就阻止掉。

但这样依然不能完全解决问题,万一你用的浏览器也是有问题的,能伪造 referer 呢?

所以一般会用随机值来解决,每次登录随机生成一个值,放到 session 中,后面的请求需要包含这个值才行,否则就认为是非法的。

这个随机值叫做 token,可以放在参数中,也可以放在 header 中,因为钓鱼网站拿不到这个随机值,就算带了 cookie 也没发通过服务端的验证。

这是 session + cookie 这种方案的一个缺点,但是是有解决方案的。

它还有别的缺点,比如分布式的时候:

分布式 session

session 是把状态数据保存在服务端,那么问题来了,如果有多台服务器呢?

当并发量上去了,单台服务器根本承受不了,自然需要做集群,也就需要多台服务器来提供服务。

而且现在后端还会把不同的功能拆分到不同的服务中,也就是微服务架构,自然也需要多台服务器。

那不同服务器之间的 session 怎么同步?

登录之后 session 是保存在某一台服务器的,之后可能会访问到别的服务器,这时候那台服务器是没有对应的 session 的,就没法完成对应的功能。

这个问题的解决有两种方案:

一种是 session 复制,也就是通过一种机制在各台机器自动复制 session,并且每次修改都同步下。这个有对应的框架来做,比如 java 的 spring-session。

各台服务器都做了 session 复制了,那你访问任何一台都能找到对应的 session。

还有一种方案是把 session 保存在 redis,这样每台服务器都去那里查,只要一台服务器登录了,其他的服务器也就能查到 session,这样就不需要复制了。

还好,session 在分布式时的这个问题也算是有解决方案的。

但你你以为这就完了么?session + cookie 还有跨域的问题:

跨域

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

而且还可以设置过期时间、路径等:

那万一是不同 domain 的请求呢?也就是跨域的时候,怎么带 cookie 呢?

a.guang.com 和 b.guang.com 这种还好,只要把 domain 设置为顶级域名 guang.com 就可以了,那二三级域名不同也能自动带上。

但如果顶级域名也不同就没办法了,这种只能在服务端做下中转,把这俩个域名统一成同一个。

上面说的不是 ajax 请求,ajax 请求有额外的机制:

ajax 请求跨域的时候是不会挟带 cookie 的,除非手动设置 withCredentials 为 true 才可以。

而且也要求后端代码设置了对应的 header:

Access-Control-Allow-Origin: "当前域名";
Access-Control-Allow-Credentials: true
复制代码

这里的 allow origin 设置 * 都不行,必须指定具体的域名才能接收跨域 cookie。

这是 session + cookie 方式的第三个坑,好在也是有解决方案的。

我们做下小结:

session + cookie 的给 http 添加状态的方案是服务端保存 session 数据,然后把 id 放入 cookie 返回,cookie 是自动携带的,每个请求可以通过 cookie 里的 id 查找到对应的 session,从而实现请求的标识。这种方案能实现需求,但是有 CSRF、分布式 session、跨域等问题,不过都是有解决方案的。

session + cookie 的方案确实不太完美,我们再来看另一种方式怎么样:

客户端存储的 token

session + cookie 的方案是把状态数据保存在服务端,再把 id 保存在 cookie 里来实现的。既然这样的方案有那么多的问题,那我反其道而行之,不把状态保存在服务端了,直接全部放在请求里,也不放在 cookie 里了,而是放在 header 里,这样是不是就能解决那一堆问题了呢?

token 的方案常用 json 格式来保存,叫做 json web token,简称 JWT,我们就拿这个来说吧。

JWT 是保存在 request header 里的一段字符串(比如用 header 名可以叫 authorization),它分为三部分:

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

header 部分保存当前的加密算法,payload 部分是具体存储的数据,verify signature 部分是把 header 和 payload 还有 salt 做一次加密之后生成的。(salt,盐,就是一段任意的字符串,增加随机性)

这三部分会分别做 Base64,然后连在一起就是 JWT 的 header,放到某个 header 比如 authorization 中:

authorization: barer xxxxx.xxxxx.xxxx
复制代码

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

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

而且这种方式是没有 session + cookie 那些问题的,不信我们分别来看一下:

CSRF:因为不是通过自动带的 cookie 来关联服务端的 session 保存的状态,所以没有 CSRF 问题,没法通过 cookie 攻击。

分布式 session: 因为状态不是保存在服务端,所以无论访问哪台服务器都行,只要能从 token 里解析出状态数据就行。

跨域:因为不是 cookie 那一套,自然也没有跨域的限制,只要手动带上 JWT 的 header 就行。

看起来这种方式好像很完美?

其实也不是,JWT 有 JWT 的问题:

安全性

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

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

性能

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

所以 JWT 里也不要保存太多数据。

没法让 JWT 失效

session 因为是存在服务端的,那我们就可以随时让它失效,而 JWT 不是,因为是保存在客户端,那我们是没法手动让他失效的。

比如踢人、退出登录、改完密码下线这种功能就没法实现。

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

所以说,JWT 的方案虽然解决了很多 session + cookie 的问题,但也不完美。

小结下:

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

说了这么多,还是写下代码心里更踏实:

Nest.js 实现两种方案

我们用 Nest.js 实现下两种方案吧,不能光纸上谈兵。

首先用 @nest/cli 快速创建一个 Nest.js 项目

npx nest new status
复制代码

会生成 module、controller、service 的基础代码:

我们先实现 session + cookie 的方式:

session + cookie

Nest.js 的底层是 express,它只是额外提供了一些架构的划分,所以还是 session 实现还是用的 express 的方案:

安装 express-session 和它的 ts 类型定义:

npm install express-session @types/express-session
复制代码

然后在入口模块里启用它:

指定个加密 cookie 用的密码就行。

然后在 controller 里就可以注入 session 对象了:

我在 session 里放了个 count 的变量,每次访问加一,然后 body 返回这个 count。

这样就可以判断 http 请求是否有了状态。

我们来测试下:

可以看到每次请求返回的数据都不同,而且返回了一个 cookie 是 connect.sid,这个就是对应 session 的 id。

因为 cookie 在请求的时候会自动带上,就可以实现请求的标识,给 http 请求加上状态。

session + cookie 的方式用起来还是很简单的,我们再来看下 jwt 的方式:

jwt

jwt 需要引入 @nestjs/jwt 这个包,然后在入口 Module 里引入 JwtModule:

引入的时候指定密码,也就是用来加到 jwt 里的盐,也可以指定 token 过期时间。

因为我们引入了 JwtModule,那就可以在 Controller 里依赖注入了:

声明对 JwtService 的依赖,Nest.js 就会自动注入对应的对象

然后定义个 controller 方法,通过 Resonse 对象来设置 authorization 的 header:

用 jwtService 生成一个 token,记录 count,然后放到 header 里返回,同时也放在 body 里。

后面的请求就是取出这个 header,拿到其中的数据,然后 +1 之后再放回去:

这样也实现了给 http 添加状态的需求,不过是把数据保存在了 header 里。

我们通过 postman 测试下:

第一次请求会返回一个 authorization 的 header,body 是 1:

把这个 header 手动添加到请求 header 里,再次请求:

body 变成 2 了,同时也返回了一个新的 authorization 的 header。

把这个新的 authorization 放到请求 header 里再次请求:

El cuerpo se convierte en 3 y también se devuelve un nuevo encabezado de autorización.

Algunos estudiantes preguntaron qué pasa si no necesito un nuevo encabezado o uso el último encabezado:

Eso dará un error:

La generación jwt solo se puede usar una vez, y esta única vez también es una característica de la misma.

De esta forma, usamos Nest.js para implementar dos formas de guardar el estado http, sesión + cookie y jwt respectivamente.

Código subido a github: github.com/QuarkGluonP…

Resumir

HTTP no tiene estado, es decir, no hay asociación entre solicitudes y solicitudes, pero la implementación de muchas de nuestras funciones necesita guardar estado.

Hay dos formas de agregar estado a http:

sesión + cookie : guarde los datos de estado en el servidor, coloque la identificación de la sesión en la cookie y devuélvala, de modo que cada solicitud traiga la cookie y la sesión correspondiente se pueda encontrar a través de la identificación. Esta solución tiene problemas de CSRF, sesión distribuida y entre dominios.

jwt : guarde el estado en un token con formato json y colóquelo en el encabezado, que debe traerse manualmente. No hay problemas de cookies + sesión, pero también hay problemas de seguridad, rendimiento, incontrolables y de uso único.

Ninguna de las soluciones anteriores es perfecta, pero hay soluciones para esos problemas.

Este es el caso en muchos casos en el campo del software, una determinada solución resuelve algunos problemas, pero también trae algunos problemas nuevos en consecuencia. No existe una bala de plata, pero aún debe estar familiarizado con sus características y elegirlas de manera flexible de acuerdo con las diferentes necesidades.

Supongo que te gusta

Origin juejin.im/post/7078672940492914725
Recomendado
Clasificación