HTTPに状態を追加する両方の方法は完全ではありません

この記事は、Node.jsAdvancedRoadのテクニカルトピックコールに参加しています。クリックして詳細を表示してください"

httpはステートレスであることがわかっています。つまり、前のリクエストと次のリクエストの間に相関関係はありません。ただし、アプリケーションの機能を実装する場合は、多くの場合、ステートフルである必要があります。たとえば、ログインしてからショッピングカートに追加した後、ログインしたユーザーがそれを実行したことを認識する必要があります。

httpリクエストにステータスを追加するにはどうすればよいですか?

この問題には2つの解決策があります。サーバー側に保存されているセッション+Cookieの解決策と、クライアント側に保存されているトークンの解決策です。

しかし実際には、これら2つのスキームはあまり良くなく、完璧ではありません。

なんでそんなこと言うの?それらを別々に見てみましょう:

サーバーに保存されているセッション+Cookie

httpに状態を追加するには、各リクエストにマークを付けてから、このマークに対応するデータをサーバーに保存します。このようにして、マークされた各リクエストは対応するデータを見つけることができ、ログインやアクセス許可などのステータスを保存するのは自然なことです。

このタグは自動的に運ばれるはずなので、httpはCookieメカニズムを設計しており、タグに保存されているデータはリクエストごとに運ばれます。

次に、Cookie内のタグに従って、サーバー側の対応するデータはセッションと呼ばれ、このタグはセッションのIDです。

図に示すように、リクエストは自動的にCookieをもたらすため、両方のリクエストでID 1に対応するセッションを見つけることができ、現在ログインしているユーザーが誰であるかを知ることは自然であり、他のステータスを保存することもできますデータ。

これは、httpに状態を追加するためのセッション+Cookieスキームです。

この計画に問題があると思いますか?

問題があり、かなりの数があります。

最大の問題の1つは、悪名高いCSRF(Cross Site Request Forgery)です。

CSRF

クッキーはリクエスト時に自動的に運ばれるため、あるウェブサイトにログインしてから別のウェブサイトにアクセスした場合、前のウェブサイトをリクエストするボタンがそこにあれば、クッキーを運ぶことができます。そして、もうログインする必要はありません。

では、このボタンを押した後に危険な操作が行われた場合はどうなるでしょうか。

それは危険ですか。

また、一般的に、CSRFの脆弱性を利用するWebサイトは偽装されているため、欠陥がわかりにくくなっています。このようなWebサイトはフィッシングWebサイトと呼ばれます。

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

本文は3になり、新しい認証ヘッダーも返されます。

一部の学生は、新しいヘッダーが必要ない場合、または最後のヘッダーを使用する場合はどうなるかと尋ねました。

エラーが発生します:

jwt生成は1回しか使用できず、この1回だけがその機能です。

このように、Nest.jsを使用して、http状態を保存する2つの方法、セッション+cookieとjwtをそれぞれ実装します。

githubにアップロードされたコード:github.com/QuarkGluonP…

要約する

HTTPはステートレスです。つまり、リクエストとリクエストの間に関連付けはありませんが、多くの関数の実装では状態を保存する必要があります。

httpにステータスを追加する方法は2つあります。

session + cookie:状態データをサーバーに保存し、セッションIDをcookieに入れて返すことで、各リクエストがcookieを取得し、対応するセッションをidから見つけることができます。このソリューションには、CSRF、分散セッション、およびクロスドメインの問題があります。

jwt:状態をjson形式のトークンに保存し、ヘッダーに配置します。これは手動で取得する必要があります。Cookieとセッションの問題はありませんが、セキュリティ、パフォーマンス、制御不能、および一度使用した問題もあります。

上記の解決策はどちらも完璧ではありませんが、これらの問題に対する解決策はあります。

これはソフトウェア分野の多くの場合に当てはまります。特定のソリューションはいくつかの問題を解決しますが、それに応じていくつかの新しい問題ももたらします。特効薬はありませんが、それでもその特性に精通し、さまざまなニーズに応じて柔軟に選択する必要があります。

おすすめ

転載: juejin.im/post/7078672940492914725