Both ways to add state to HTTP are not perfect

This article is participating in the technical topic call for Node.js advanced road, click to view the details "

We know that http is stateless, which means that there is no correlation between the previous request and the next request. However, if we want to implement the functions of the application, we often need to be stateful. For example, after logging in, and then adding to the shopping cart, it should be recognized that it is done by the logged-in user.

How to add status to http request?

There are two solutions to this problem: the solution of session + cookie stored on the server side, and the solution of token stored on the client side.

But in fact, these two schemes are not very good, and they are not perfect.

Why do you say that? Let's take a look at them separately:

Session + cookie stored on the server

To add a state to http, mark each request with a mark, and then store the data corresponding to this mark on the server. In this way, each marked request can find the corresponding data, and it is natural to store the status such as login and permissions.

This tag should be automatically carried, so http has designed a cookie mechanism, and the data stored in it will be carried with each request.

Then according to the tag in the cookie, the corresponding data on the server side is called session, and this tag is the id of the session.

As shown in the figure, because the request automatically brings a cookie, the session corresponding to the id of 1 can be found in both requests, and it is natural to know who the currently logged-in user is, and can also store other status data.

This is the session + cookie scheme to add state to http.

Do you think there is a problem with this plan?

There are problems, and there are quite a few.

One of the biggest problems is the infamous CSRF (Cross Site Request Forgery):

CSRF

Because the cookie will be automatically carried when the request is made, then if you log in to a website and then visit another website, if there is a button in it that will request the previous website, the cookie can still be carried. And then you don't have to log in anymore.

So what if some dangerous operations are done after this button is pressed?

Is it dangerous.

And generally, the websites that use CSRF vulnerabilities are well disguised, making it difficult for you to see the flaws. Such websites are called phishing websites.

为了解决这个问题,我们一般会验证 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 里再次请求:

The body becomes 3, and a new authorization header is also returned.

Some students asked, what if I don't need a new header, or use the last header:

That will give an error:

The jwt generation can only be used once, and this one-time is also a feature of it.

In this way, we use Nest.js to implement two ways of saving http state, session + cookie and jwt respectively.

Code uploaded to github: github.com/QuarkGluonP…

Summarize

HTTP is stateless, that is, there is no association between requests and requests, but the implementation of many of our functions needs to save state.

There are two ways to add status to http:

session + cookie : save the status data to the server, and put the session id in the cookie and return it, so that each request will bring the cookie, and the corresponding session can be found through the id. This solution has CSRF, distributed session, and cross-domain problems.

jwt : save the state in the json format token and put it in the header, which needs to be brought manually. There are no problems of cookie + session, but there are also problems of security, performance, uncontrollable and failure after one use.

Neither of the above solutions are perfect, but there are solutions to those problems.

This is the case in many cases in the software field. A certain solution solves some problems, but also brings some new problems accordingly. There is no silver bullet, but you still need to be familiar with their characteristics and choose them flexibly according to different needs.

Guess you like

Origin juejin.im/post/7078672940492914725