# JWT 疑惑记录

JWT 即 JSON Web Token,是目前最流行的跨域认证解决方案。

更详细的看这个,以下细节也来自于这里 (opens new window)

cookie 和 session 在最初的认证登录时是一对功能点。cookie 体现在前端,用它来保存认证过的标记(sessionId);session 体现在后端,用来保存认证过的信息(用户名、密码...)。

  • 空间限制(Domain/Path): cookie 设置带有空间范围限制,通过 Domain(域名) 或者 Path(路径) 两级处理
  • 时间限制(Expires/Max-Age): cookie 设置时需要带上时间处理,通过 Expires(具体的时间)、Max-Age(一个毫秒数)中的一种。两种同时存在,Max-Age 优先级更高。如果这两个都没有,就是会话 cookie,浏览器关闭即消失。
  • secure: 浏览器只有在加密协议 HTTPS 下,才能将这个 Cookie 发送到服务器。https 下自动开启。
  • httpOnly: 让 cookie 无法通过 JavaScript 脚本拿到,主要是 Document.cookie 属性、XMLHttpRequest 对象和 Request API 都拿不到该属性。

响应头中设置 Set-Cookie

// 设置一个
Set-Cookie: username=name; domain=yuming.com; path=/blog; Expires=Wed, 21 Oct 2025 07:28:00 GMT; Secure; HttpOnly


// 当然一次 HTTP 请求中允许重复设置 cookie
Set-Cookie: username=name; domain=yuming.com
Set-Cookie: height=180; domain=me.yuming.com
Set-Cookie: weight=80; domain=me.yuming.com

那么 session 是如何搭上了 cookie 这艘破船的呢?先来张图看看:

An image

  • 浏览器登录发送账号密码,服务端查用户数据库,校验用户
  • 校验成功后,服务端把用户登录状态存为 Session(它可能包含用户信息、session 状态等),同时生成一个 sessionId
  • 登录接口返回时通过 Set-Cookie 把 sessionId 保存到 cookie 上
  • 此后浏览器再请求业务接口,sessionId 随 cookie 带上
  • 服务端查 sessionId 校验 session
  • 成功后正常做业务处理,返回结果

# 图中的 Session 库

之前最大的困惑在于,session 为啥老被说成占用内存占用资源 或者 ,了解一下 session 存储方式就好了。

  • Redis(推荐):内存型数据库,以 key-value 的形式存,正合 sessionId-sessionData 的场景;且访问快。
  • 内存:直接放到变量里。一旦服务重启就没了
  • 数据库:普通数据库。性能不高。

看到了吧,保存到变量里占当前服务器内存且容易丢。保存到数据库,就是慢,具体为啥慢不懂。最后就是 Redis,快且不占内存。

但是,后来有了分布式的概念,和集群啥区别我也不懂 🙈🙈🙈

反正个人理解是有了集群就有了负载均衡这个高大上的词,所有的服务先走人家 nginx, nginx 知道当时哪个机器不忙就给你传送过去。但是哈,但是,你万一是在另一个机器上登录的,不忙的机器不知道,那直接变量存储放到内存里那就更不靠谱了。所以,数据库保存或者 redis 保存就更重要了。

如果量比较大呢?所以,你得单独搞个服务来处理数据库或者 redis 来存储这些信息,于是就有人说,不行,这太占用资源,太浪费钱了,。

你看,还是不太行。

有咩有一种方法,我服务端不保存这些 sessionData 信息,靠逻辑发电。

你来登录,我处理你的登录信息,搞个 标识 出来,再发给你,你下次来带着它,我在逻辑里解析这玩意,一解析发现这就是我的东西,那我就认为 We are 伐木累~

# 于是就有了 token

# token 登录流程图

先看图,先看图:

An Image

  • 用户登录,服务端校验账号密码,获得用户信息
  • 把用户信息、密钥 配置编码成 token,还通过 Set-Cookie 保存到浏览器
  • 此后用户请求业务接口,通过 cookie 携带 token
  • 接口校验 token 有效性,进行正常业务接口处理

# 用 nodejs 举个例子

nodejs 通过 cookie-session 库就可以处理 token 的形式。

看下面的代码:

const express = require("express");
const cookieSession = require("cookie-session");
const cookieParser = require("cookie-parser");

const server = express();

server.use(cookieParser());
server.use(
  cookieSession({
    name: "token", // 存储到 cookie 的key值,可以不设置,有默认值
    signed: false, // 是否给 cookie 进行签名处理,为false才可以不提供密钥
    maxAge: 2 * 3600 * 1000,
  })
);

server.use("/", function (req, res) {
  req.session = { userid: "12345678" };

  res.send("ok");
});

server.listen(3002, () => {
  console.log("server is run at 3002");
});

当前的情况下,通过设置 req.session 就将 {"userid":"12345678"} 保存到了前端,长这个样子:

An Image

这里的 eyJ1c2VyaWQiOiIxMjM0NTY3OCJ9,就是 {"userid":"12345678} 的 base64 而已。注意,真是转化是是标准 json 形式 {"key":"value"} 中间没有空格。

如果有个大鸟,他手动转了一个 {"userid":"12345678} 的 base64,然后替换了自己的 cookie,就可以直接访问 userid 是 12345678 的信息了,这太可怕了。

所以,得防止篡改。给当前的 token 加签名处理。

cookie-session 库提供了两个配置:

# 用于签署和验证 cookie 值的密钥列表,和 secret 相当,可以简单的理解为多个配置。
# keys: ['key1', 'key2', 'key3']

# 签名密钥
secret: 'harryport',

# 是否给cookie进行签名处理
signed: true,

所以代码更改成了这样:

const express = require("express");
const cookieSession = require("cookie-session");
const cookieParser = require("cookie-parser");

const server = express();

server.use(cookieParser());
server.use(
  cookieSession({
    name: "token", // 存储到 cookie 的key值,可以不设置,有默认值
    signed: true, // 是否给 cookie 进行签名处理,设置为 true 必须有 secret 或 keys配置
    secret: "key", // 密钥
    maxAge: 2 * 3600 * 1000,
  })
);

server.use("/", function (req, res) {
  req.session = { userid: "12345678" };
  res.send("ok");
});

server.listen(3002, () => {
  console.log("server is run at 3002");
});

设置完后,cookie 的存储中会多出一个带 .sig 的 cookie,它就是 {"userid":"12345678} 和 密钥 key 通过加密算法计算出来的。前端保存如下:

An Image

它的签名过程如下:

An Image

所以一个登录有了俩 cookie,别人可以模拟只有 {"userid":"abb”},却模拟不了 .sig 的 cookie,因为他不知道你的密钥。

但是,总有但是,我不喜欢俩 cookie 咋整?

# 于是有了 JWT

jwt 一般都长这样,三个点连接三个不知道是啥的啥:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJhIiwiaWF0IjoxNTUxOTUxOTk4fQ.2jf3kl_uKWRkwjOP6uQRJFqMlwSABcgqqcJofFH5XCo

他们分别是 Header(头部).Payload(负载).Signature(签名)

An Image

# 头部用来声明类型和加密类型

An Image

# 负载是一些别的信息

An Image

# 签名

签名的生成来源于两部分:

  1. Base64(header) + . + Base64(payload)
  2. 配合 secret 使用 header 里指定的加密算法对第一部分的组合生成 签名。
加密算法( Base64(header).Base64(payload), secret )

# 认证流程

  1. 用户登录时,服务端认证成功后,返回给客户端一个 token

  2. 客户端可以将这个 token 保存到本地,localstorage 和 cookie 都可以

  3. 后续的接口请求中,需要在请求头中设置 Authorization 字段来带上本地保存的 token。Authorization 设置是有固定模式的:Bearer token值

Authorization: Bearer <token>
  1. 服务端会检查请求头中 Authorization 字段是否合法,如果合法则允许当前的请求操作。

# 说点优点

和 session 模式比较的话。。。

  • JWT 是自包含的(内部包含了一些会话信息),因此减少了需要查询数据库的需要

  • JWT 并不使用 Cookie 的,所以你可以使用任何域名提供你的 API 服务而不需要担心跨域资源共享问题(CORS)

  • 因为用户的状态不再存储在服务端的内存中,所以这是一种无状态的认证机制

和 cookie 比较的话。。。 当你存储到 cookie 中时,这只需要一个 cookie 就可以了。