Skip to content

JWT ✨

要点速览

  • JWT 是一种令牌格式,不限定存储与传输位置;常用 Authorization: Bearer <token>
  • 令牌结构:header.payload.signatureheader/payload 为 base64url 编码的 JSON,signature 用指定算法签名。
  • 安全本质:signature 保证不可伪造与篡改;payload 非加密,属于可读内容。
  • 算法选择:HS256(对称密钥)与 RS256(非对称密钥);服务端需安全管理密钥。
  • 过期与校验:服务器自行验证签名、exp/nbf/iat/aud/iss 等声明;库仅提供基础能力,策略由业务控制。

概述

回顾登录的流程:

接下来的问题是:这个出入证(令牌)里面到底存啥?

一种比较简单的办法就是直接存储用户信息的 JSON 串,这会造成下面的几个问题:

  • 非浏览器环境,如何在令牌中记录过期时间
  • 如何防止令牌被伪造

JWT 就是为了解决这些问题出现的。

JWT 全称Json Web Token,本质就是一个字符串

它要解决的问题,就是在互联网环境中,提供统一的、安全的令牌格式

因此,JWT 只是一个令牌格式而已,你可以把它存储到 cookie,也可以存储到 localstorage,没有任何限制!

同样的,对于传输,你可以使用任何传输方式来传输 JWT,一般来说,我们会使用消息头来传输它

比如,当登录成功后,服务器可以给客户端响应一个 JWT:

HTTP/1.1 200 OK
Set-Cookie: token=JWT令牌
Authorization: Bearer JWT令牌
Content-Type: application/json

{ "token": "JWT令牌" }

可以看到,JWT 令牌可以出现在响应的任何一个地方,客户端和服务器自行约定即可

当然,它也可以出现在响应的多个地方,比如为了充分利用浏览器的 cookie,同时为了照顾其他设备,也可以让 JWT 出现在 set-cookie 和 authorization 或 body 中,尽管这会增加额外的传输量。

当客户端拿到令牌后,它要做的只有一件事:存储它。

你可以存储到任何位置,比如手机文件、PC 文件、localstorage、cookie

当后续请求发生时,你只需要将它作为请求的一部分发送到服务器即可。

虽然 JWT 没有明确要求应该如何附带到请求中,但通常我们会使用如下的格式

GET /api/resources HTTP/1.1
Authorization: Bearer JWT令牌

这样一来,服务器就能够收到这个令牌了,通过对令牌的验证,即可知道该令牌是否有效。

它们的完整交互流程是非常简单清晰的

令牌的组成

为了保证令牌的安全性,JWT 令牌由三个部分组成,分别是:

  1. header:令牌头部,记录了整个令牌的类型和签名算法
  2. payload:令牌负荷,记录了保存的主体信息,比如你要保存的用户信息就可以放到这里
  3. signature:令牌签名,按照头部固定的签名算法对整个令牌进行签名,该签名的作用是:保证令牌不被伪造和篡改

它们组合而成的完整格式是:header.payload.signature

比如,一个完整的 JWT 令牌如下:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9.BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

它各个部分的值分别是:

  • header:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • payload:eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9
  • signature: BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

下面分别对每个部分进行说明

它是令牌头部,记录了整个令牌的类型和签名算法

它的格式是一个 JSON 对象,如下:

json
{ "alg": "HS256", "typ": "JWT" }

该对象记录了:

  • alg:signature 部分使用的签名算法,通常可以取两个值
    • HS256:一种对称加密算法,使用同一个秘钥对 signature 加密解密
    • RS256:一种非对称加密算法,使用私钥签名,公钥验证
  • typ:整个令牌的类型,固定写JWT即可

设置好了 header 之后,就可以生成 header 部分了

具体的生成方式及其简单,就是把header部分使用base64 url编码即可

base64url 不是一个加密算法,而是一种编码方式,它是在 base64 算法的基础上对 +、=、/ 三个字符做出特殊处理的算法

base64是使用 64 个可打印字符来表示一个二进制数据,具体的做法参考百度百科

浏览器提供了 btoa 函数,可以完成这个操作:

js
window.btoa(
  JSON.stringify({
    alg: "HS256",
    typ: "JWT",
  })
);
// 得到字符串:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

同样的,浏览器也提供了 atob 函数,可以对其进行解码:

js
window.atob("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9");
// 得到字符串:{"alg":"HS256","typ":"JWT"}

nodejs 中没有提供这两个函数,可以安装第三方库或使用 Buffer 实现。

payload

这部分是 JWT 的主体信息,它仍然是一个 JSON 对象,它可以包含以下内容:

json
{
  "ss""发行者",
  "iat""发布时间",
  "exp""到期时间",
  "sub""主题",
  "aud""听众",
  "nbf""在此之前不可用",
  "jti""JWT ID"
}

以上属性可以全写,也可以一个都不写,它只是一个规范,就算写了,也需要你在将来验证这个 JWT 令牌时手动处理才能发挥作用

上述属性表达的含义分别是:

  • ss:发行该 JWT 的是谁,可以写公司名字,也可以写服务名称
  • iat:该 JWT 的发放时间,通常写当前时间的时间戳
  • exp:该 JWT 的到期时间,通常写时间戳
  • sub:该 JWT 是用于干嘛的
  • aud:该 JWT 是发放给哪个终端的,可以是终端类型,也可以是用户名称,随意一点
  • nbf:一个时间点,在该时间点到达之前,这个令牌是不可用的
  • jti:JWT 的唯一编号,设置此项的目的,主要是为了防止重放攻击(重放攻击是在某些场景下,用户使用之前的令牌发送到服务器,被服务器正确的识别,从而导致不可预期的行为发生)

可是到现在,看了半天,没有出现我想要写入的数据啊 😂

当用户登陆成功之后,我可能需要把用户的一些信息写入到 JWT 令牌中,比如用户 id、账号等等(密码就算了 😳)

其实很简单,payload 这一部分只是一个 json 对象而已,你可以向对象中加入任何想要加入的信息

比如,下面的 json 对象仍然是一个有效的 payload

json
{
  "foo": "bar",
  "iat": 1587548215
}

foo: bar是我们自定义的信息,iat: 1587548215是 JWT 规范中的信息

最终,payload 部分和 header 一样,需要通过 base64url 编码得到

js
window.btoa(
  JSON.stringify({
    foo: "bar",
    iat: 1587548215,
  })
);
// 得到字符串:eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9

signature

这一部分是 JWT 的签名,正是它的存在,保证了整个 JWT 不被篡改

这部分的生成,是对前面两个部分的编码结果,按照头部指定的方式进行签名

比如:头部指定的加密方法是HS256,前面两部分的编码结果是eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9

则第三部分就是用对称算法 HS256 对字符串进行签名(HMAC-SHA256),需指定一个密钥。

js
HS256(
  `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9`,
  "shhhhh"
);
// 得到:BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

最终,将三部分组合在一起,就得到了完整的 JWT

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIiLCJpYXQiOjE1ODc1NDgyMTV9.BCwUy3jnUQ_E6TqCayc7rCHkx-vxxdagUwPOWqwYCFc

由于签名使用的密钥保存在服务器,这样一来,客户端就无法伪造出签名,因为它拿不到密钥。

换句话说,之所以说无法伪造 JWT,就是因为第三部分的存在。

前面两部分并没有加密,只是一个编码结果而已,可以认为几乎是明文传输

这不会造成太大的问题,因为既然用户登陆成功了,它当然有权力查看自己的用户信息

甚至在某些网站,用户的基本信息可以被任何人查看

你要保证的,是不要把敏感的信息存放到 JWT 中,比如密码

JWT 的signature可以保证令牌不被伪造,那如何保证令牌不被篡改呢?

比如,某个用户登陆成功了,获得了 JWT,但他人为的篡改了payload,比如把自己的账户余额修改为原来的两倍,然后重新编码出payload发送到服务器,服务器如何得知这些信息被篡改过了呢?

这就要说到令牌的验证了

生成与验证示例(Node.js)

js
const crypto = require("crypto");

function base64url(input) {
  return Buffer.from(input)
    .toString("base64")
    .replace(/=/g, "")
    .replace(/\+/g, "-")
    .replace(/\//g, "_");
}

function signHS256(data, secret) {
  return crypto
    .createHmac("sha256", secret)
    .update(data)
    .digest("base64")
    .replace(/=/g, "")
    .replace(/\+/g, "-")
    .replace(/\//g, "_");
}

function createToken(payload, secret) {
  const header = { alg: "HS256", typ: "JWT" };
  const h = base64url(JSON.stringify(header));
  const p = base64url(JSON.stringify(payload));
  const s = signHS256(`${h}.${p}`, secret);
  return `${h}.${p}.${s}`;
}

function verifyToken(token, secret) {
  const [h, p, s] = token.split(".");
  if (!h || !p || !s) return { ok: false, reason: "format" };
  const expect = signHS256(`${h}.${p}`, secret);
  if (expect !== s) return { ok: false, reason: "signature" };
  const payload = JSON.parse(
    Buffer.from(p.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString(
      "utf8"
    )
  );
  const now = Math.floor(Date.now() / 1000);
  if (payload.nbf && now < payload.nbf) return { ok: false, reason: "nbf" };
  if (payload.exp && now >= payload.exp) return { ok: false, reason: "exp" };
  return { ok: true, payload };
}

const secret = "shhhhh";
const payload = {
  sub: "user:123",
  iat: Math.floor(Date.now() / 1000),
  exp: Math.floor(Date.now() / 1000) + 3600,
};
const token = createToken(payload, secret);
const res = verifyToken(token, secret);

算法与环境

  • 浏览器环境可使用 Web Crypto API 实现 HMAC/RSASSA-PKCS1-v1_5;示例基于 Node.js 标准库。
  • RS256 需管理私钥/公钥,支持撤销与轮换;HS256 密钥共享,适合单服务或集中密钥管理。

令牌的验证

令牌在服务器组装完成后,会以任意的方式发送到客户端

客户端会把令牌保存起来,后续的请求会将令牌发送给服务器

而服务器需要验证令牌是否正确,如何验证呢?

首先,服务器要验证这个令牌是否被篡改过,验证方式非常简单,就是对header+payload用同样的秘钥和加密算法进行重新加密

然后把加密的结果和传入 JWT 的signature进行对比,如果完全相同,则表示前面两部分没有动过,就是自己颁发的,如果不同,肯定是被篡改过了。

传入的header.传入的payload.传入的signature
新的signature = header中的加密算法(传入的header.传入的payload, 秘钥)
验证:新的signature == 传入的signature

当令牌验证为没有被篡改后,服务器可以进行其他验证:比如是否过期、听众是否满足要求等等,这些就视情况而定了

注意:这些验证都需要服务器手动完成;第三方库(如 jsonwebtoken)提供签名与基础校验,具体策略由业务控制。

常用声明与作用

  • iss 发行者、aud 听众:限定令牌来源与使用方。
  • exp 过期时间、nbf 生效时间、iat 签发时间:控制时效性与防重放窗口。
  • jti 唯一 ID:结合服务端黑名单/撤销列表实现失效与踢出。

安全注意

  • 不在 payload 存放敏感数据;payload 非加密。
  • HTTPS 传输,防止中间人窃听与篡改。
  • 妥善管理密钥,支持轮换与撤销;区分测试与生产密钥。
  • 与 Cookie 结合时,建议使用 HttpOnly/Secure/SameSite;防范 XSS/CSRF。

总结

最后,总结一下 JWT 的特点:

  • JWT 本质上是一种令牌格式。它和终端设备无关,同样和服务器无关,甚至与如何传输无关,它只是规范了令牌的格式而已
  • JWT 由三部分组成:header、payload、signature。主体信息在 payload
  • JWT 难以被篡改和伪造。这是因为有第三部分的签名存在。
适用场景与替代
  • 适合跨端与分布式场景的无状态鉴权;携带基础身份/权限信息。
  • 结合短期令牌与刷新令牌机制,降低泄露风险与提升续期体验。
  • 对于需要可撤销、细粒度会话控制的场景,可结合服务端会话表、Token 黑名单或采用服务器存储会话的方案。

面试题

请阐述 JWT 的令牌格式

参考答案:

token 分为三段,分别是 header、payload、signature

其中,header 标识签名算法和令牌类型;payload 标识主体信息,包含令牌过期时间、发布时间、发行者、主体内容等;signature 是使用特定的算法对前面两部分进行加密,得到的加密结果。

token 有防篡改的特点,如果攻击者改动了前面两个部分,就会导致和第三部分对应不上,使得 token 失效。而攻击者不知道加密秘钥,因此又无法修改第三部分的值。

所以,在秘钥不被泄露的前提下,一个验证通过的 token 是值得被信任的。