Skip to content

JWT从入门到精通

JWT的概念对于我来说是来的比较晚的了,但并不妨碍它是一个优秀的跨域认证方案。

一、什么是token

JWT(JSON Web Token)是一种用于身份验证和信息安全传输的开放标准(RFC 7519)。它以 JSON 格式存储信息,并使用数字签名确保安全性,常用于前后端分离认证和无状态身份验证。

alt text

二、JWT的特点

  • 无状态 & 轻量化(Stateless & Lightweight)
    • JWT 本身包含了用户信息,不依赖服务器存储 Session,适用于分布式系统。
    • 服务器只需验证 JWT 的签名,不需要维护会话数据,减少服务器压力。
  • 安全性(Security)
    • 签名机制:JWT 使用 HMAC(HS256)或 RSA(RS256)进行签名,防止数据篡改。
    • 支持加密:虽然 JWT 默认不加密,但可以用 JWE(JSON Web Encryption)加密 Payload,保护敏感信息。
  • 跨平台 & 兼容性(Cross-platform & Compatibility)
    • JWT 采用 JSON 格式,前端(JavaScript)、后端(Node.js、Python、Java 等)、移动端(iOS、Android)都支持。
    • 适用于 Web、APP、小程序、微服务等各种场景。
  • 自包含(Self-contained)
    • JWT 里面包含了用户的身份信息(如 userIdroleexp 等),服务器可以直接解析使用,无需数据库查询。
  • 易于传输(Compact & URL-safe)
    • JWT 是 Base64 编码的字符串,体积小,适合通过 HTTP 头部、URL、Cookie 进行传输。
    • 示例:
    js
    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
      .eyJ1c2VySWQiOjEyMywicm9sZSI6ImFkbWluIiwiZXhwIjoxNzAwMDAwMDB9
      .KkN3H8J1JZ5LnpI6LbXG - KI9mJHbRkoRCQ;
  • 支持 Token 过期控制(Expiration)
    • 可以通过 exp(过期时间)和 iat(签发时间)控制 JWT 有效期,防止 Token 长期有效带来的安全隐患。
    • 示例:
    json
    { "userId": 123, "role": "admin", "exp": 1700000000 }
  • 可扩展(Extendable)
    • JWT 除了基本的用户身份信息,还可以自定义 Payload,存储额外的数据,如 permissions(权限)、iat(签发时间)等。”

三、JWT的数据结构

JWT(JSON Web Token)由 三个部分 组成:

  1. Header(头部)
  2. Payload(载荷)
  3. Signature(签名)

它们之间用 .(点号) 连接,例如:xxxxx.yyyyy.zzzzz

alt text

1. Header(头部)


头部通常包含两个部分:

  • alg(算法):用于指定签名的加密算法(如 HS256、RS256)。
  • typ(类型):标明 Token 类型,通常是 "JWT"

示例(JSON 格式):

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

然后进行 Base64 编码,形成 JWT 的第一部分。

2. Payload(载荷)


Payload 主要包含 声明(Claims),用于存储用户信息和其他自定义数据。 常见的 Claims:

  • 标准声明(Registered Claims)
    • iss(Issuer):签发者
    • sub(Subject):主题(通常是用户 ID)
    • aud(Audience):接收者
    • exp(Expiration):过期时间(时间戳,Unix 时间)
    • iat(Issued At):签发时间
    • nbf(Not Before):生效时间
  • 自定义声明(Custom Claims): 你可以添加任何自定义字段,比如 userIdrolepermissions 等。

示例(JSON 格式):

json

json
{ "sub": "1234567890", "name": "John Doe", "role": "admin", "exp": 1710000000 }

然后进行 Base64 编码,形成 JWT 的第二部分。

3. Signature(签名)


签名用于确保 JWT 数据完整性,防止被篡改。

签名的计算方式如下:

js
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret);
  • 前两部分(Header + Payload) 进行 Base64 编码
  • 用密钥(Secret)进行加密,生成签名 如果 JWT 被篡改,签名就会不匹配,服务器会拒绝请求。 示例(伪代码)
js
HMACSHA256(
  `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
   eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzEwMDAwMDAwfQ
  `,
  "your-secret-key",
);

完整 JWT 示例


js
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  .eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzEwMDAwMDAwfQ
  .SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c;

其中:

  • 第一部分(Header)eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
  • 第二部分(Payload)eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIiwiZXhwIjoxNzEwMDAwMDAwfQ
  • 第三部分(Signature)SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

四、JWT的基本用法

1. 生成 JWT

js
const jwt = require("jsonwebtoken");

const payload = { userId: 123, role: "admin" };
const secretKey = "your-secret-key";
const token = jwt.sign(payload, secretKey, { expiresIn: "1h" });

console.log("JWT:", token);

2. 验证 JWT

js
try {
  const decoded = jwt.verify(token, secretKey);
  console.log("Decoded:", decoded);
} catch (err) {
  console.log("Invalid token");
}

五、JWT存在的问题

1. 无法主动撤销(Token 失效控制问题)

问题:JWT 生成后,如果没有内置失效机制(比如短生命周期),它会一直有效,直到过期。

  • 传统 Token 可以在服务器端存储,并在用户登出或 Token 失效时删除,但 JWT 本身是自包含的,服务器不会存储它,所以无法手动撤销。

  • 解决方案

    • 缩短 JWT 过期时间(Exp),比如 10~30 分钟,然后使用 Refresh Token 机制 让用户续签。
    • 引入黑名单机制,在 Redis 或数据库中存储已失效的 JWT,验证时检查是否在黑名单中。
    • 引入黑名单机制,代码如下(推荐)
    js
    const redisClient = require("ioredis")();
    // 用户登出时,把 Token 存入 Redis 黑名单
    app.post("/logout", async (req, res) => {
      const token = req.headers.authorization?.split(" ")[1];
      if (!token) return res.status(400).json({ message: "未提供 Token" });
    
      try {
        const decoded = jwt.verify(token, "SECRET_KEY");
    
        // 把 token 存入黑名单,设置过期时间为 token 的 exp 时间
        await redisClient.set(
          `blacklist:${token}`,
          "invalid",
          "EX",
          decoded.exp - Math.floor(Date.now() / 1000),
        );
    
        res.json({ message: "Token 已失效" });
      } catch (err) {
        res.status(401).json({ message: "无效的 Token" });
      }
    });
    
    // 服务器验证 Token 时,先检查是否在黑名单
    app.get("/protected", async (req, res) => {
      const token = req.headers.authorization?.split(" ")[1];
      if (!token) return res.status(401).json({ message: "未提供 Token" });
    
      const isBlacklisted = await redisClient.get(`blacklist:${token}`);
      if (isBlacklisted) return res.status(401).json({ message: "Token 已失效" });
    
      try {
        const decoded = jwt.verify(token, "SECRET_KEY");
        res.json({ message: "访问成功", user: decoded });
      } catch (err) {
        res.status(401).json({ message: "无效的 Token" });
      }
    });
    • **换秘钥让所有token失效(强烈不推荐)**所有用户都会重新登录,体验不好,不推荐
    js
    const SECRET_KEY = "new-secret-key";
    • Token 版本号机制(适用于修改权限)
    js
    // 用户登录时,生成一个版本号
    const SECRET_KEY = "your-secret-key";
    
    // 生成 Token(带 tokenVersion)
    app.post("/login", async (req, res) => {
      const userId = "123";
      const tokenVersion = 1; // 从数据库获取
      const token = jwt.sign({ userId, tokenVersion }, SECRET_KEY, {
        expiresIn: "1h",
      });
    
      res.json({ token });
    });
    // 保护 API(检查 tokenVersion)
    app.get("/protected", async (req, res) => {
      const token = req.headers.authorization?.split(" ")[1];
      if (!token) return res.status(401).json({ message: "未提供 Token" });
    
      try {
        const decoded = jwt.verify(token, SECRET_KEY);
    
        // 获取数据库中的 tokenVersion(假设从数据库查到 version = 2)
        const storedVersion = 2;
        if (decoded.tokenVersion !== storedVersion) {
          return res.status(401).json({ message: "Token 已失效" });
        }
    
        res.json({ message: "访问成功", user: decoded });
      } catch (err) {
        res.status(401).json({ message: "无效的 Token" });
      }
    });

2. JWT 过大,占用带宽

问题

  • JWT 包含 Header、Payload 和 Signature,通常比传统 Token 更大,尤其当 Payload 里存了很多用户信息时。
  • JWT 可能会被存入 Cookie,每次请求都会携带,影响网络性能。
  • 若使用 HTTP 头(Authorization: Bearer xxx),请求头也会变大。 解决方案
  • 只在 JWT 内存储 必要信息(比如 userId),不要存太多数据。
  • 使用压缩(比如 Brotli)减少网络传输数据量。
  • 结合 Session Token,使用更小的 Token 存 JWT,减少带宽占用。

3. 不能修改已签发的 JWT

问题:JWT 一旦生成,其内容(Payload)就不能修改,否则签名会失效。

  • 如果用户的权限变更(比如被禁用、角色修改),已签发的 JWT 仍然有效,用户可能仍然能访问某些资源。
  • 传统 Token 方案可以在数据库里更新用户权限,而 JWT 必须等它过期或者主动撤销。

解决方案

  • 使用短生命周期 JWT(比如 10 分钟),结合 Refresh Token,定期刷新用户权限。
  • 在服务器端维护 Token 版本号,如果用户权限变更,修改数据库中的版本号,JWT 校验时检查版本号是否匹配,不匹配就拒绝请求。

4. 安全性问题(可被篡改、泄露等)

(1) JWT 可被解码

  • JWT 是 Base64 编码 的,并不是加密的。任何人都可以解码 JWT,查看 Payload 里的数据。
  • 解决方案
    • 不要在 JWT 里存放敏感信息(如密码、信用卡号)。
    • 使用 HTTPS,避免中间人攻击。

(2) JWT 一旦泄露,就无法回收

  • 如果 JWT 被攻击者截获,就能伪造身份进行操作,直到 JWT 过期。

  • 解决方案

    • 使用短生命周期的 JWT,结合 Refresh Token 限制长期有效性。
    • 绑定 JWT 到 IP 或设备 ID,若请求来源 IP 变更,则拒绝请求。
    • 代码如下 生成 JWT 时,将 IP 地址和设备 ID 作为 Payload 内容的一部分。
    js
    // 首先,生成 JWT 时,可以将请求来源的 IP 地址和设备 ID 作为额外的字段加入到 Payload 中。
    app.get("/protected", (req, res) => {
      const token = req.headers.authorization?.split(" ")[1];
      if (!token) return res.status(401).json({ message: "未提供 Token" });
    
      try {
        const decoded = jwt.verify(token, SECRET);
    
        // 获取请求的 IP 和设备 ID
        const currentIp = req.ip;
        const currentDeviceId = req.body.deviceId; // 或从请求头中获取设备 ID
    
        // 验证 Token 中的 IP 和设备 ID 是否与当前一致
        if (decoded.ip !== currentIp || decoded.deviceId !== currentDeviceId) {
          return res.status(401).json({ message: "请求来源不一致,Token 失效" });
        }
        res.json({ message: "访问成功", user: decoded });
      } catch (err) {
        res.status(401).json({ message: "Token 无效" });
      }
    });
    // ==================分割线=================
    // 在后续的请求中,服务器需要验证 JWT 中存储的 IP 地址和设备 ID 是否与请求中的 IP 和设备 ID 一致。如果不一致,则拒绝请求。
    // 验证 Token 并检查 IP 和设备 ID
    app.get("/protected", (req, res) => {
      const token = req.headers.authorization?.split(" ")[1];
      if (!token) return res.status(401).json({ message: "未提供 Token" });
    
      try {
        const decoded = jwt.verify(token, SECRET);
    
        // 获取请求的 IP 和设备 ID
        const currentIp = req.ip;
        const currentDeviceId = req.body.deviceId; // 或从请求头中获取设备 ID
    
        // 验证 Token 中的 IP 和设备 ID 是否与当前一致
        if (decoded.ip !== currentIp || decoded.deviceId !== currentDeviceId) {
          return res.status(401).json({ message: "请求来源不一致,Token 失效" });
        }
    
        res.json({ message: "访问成功", user: decoded });
      } catch (err) {
        res.status(401).json({ message: "Token 无效" });
      }
    });

(3) 签名算法安全性

  • 如果使用 HS256(对称加密),密钥泄露后,任何人都能伪造 JWT。
  • 建议使用 RS256(非对称加密),公钥验证,私钥签名,保证密钥安全性。

5. 不适用于所有认证场景

JWT 适用于 无状态认证(比如 API 认证),但在 需要频繁更新权限、可控的会话管理 中表现不佳,比如:

  • 管理后台系统:管理员权限变更较频繁,JWT 不能即时更新权限,可能导致安全问题。
  • 高安全性应用(如银行系统):无法主动撤销 JWT,存在安全隐患。

解决方案

  • 对于需要动态权限管理的系统,可以使用 Session + JWT 混合方案,比如:

    • JWT 仅用于 短时间授权,用户登录后获取短期 JWT(比如 10 分钟)。
    • Session 机制存储 长期会话,用于权限管理和 Token 失效控制。
    • 代码如下
    js
    const jwt = require("jsonwebtoken");
    const redis = require("ioredis");
    const redisClient = new redis();
    const SECRET = "mySecretKey";
    
    // 生成 JWT
    function generateToken(user) {
      return jwt.sign({ id: user.id, role: user.role }, SECRET, {
        expiresIn: "1h",
      });
    }
    // API 请求时,验证 JWT & 检查黑名单
    app.get("/protected", async (req, res) => {
      const token = req.headers.authorization?.split(" ")[1];
      if (!token) return res.status(401).json({ message: "未提供 Token" });
    
      // 检查是否在黑名单
      const isBlacklisted = await redisClient.get(`blacklist:${token}`);
      if (isBlacklisted) return res.status(401).json({ message: "Token 失效" });
    
      try {
        const decoded = jwt.verify(token, SECRET);
        res.json({ message: "访问成功", user: decoded });
      } catch (err) {
        res.status(401).json({ message: "Token 无效" });
      }
    });
    
    // 修改权限 -> 让当前 JWT 失效
    app.post("/changeRole", async (req, res) => {
      const token = req.headers.authorization?.split(" ")[1];
      if (!token) return res.status(400).json({ message: "未提供 Token" });
    
      // 把当前 Token 加入黑名单(存 1 小时)
      await redisClient.set(`blacklist:${token}`, "1", "EX", 3600);
      res.json({ message: "权限变更,Token 失效,请重新登录" });
    });

6. JWT 需要额外的存储管理(Refresh Token)

问题

  • JWT 本身是无状态的,但如果使用 Refresh Token(用于刷新 JWT),就需要在服务器存储 Refresh Token,增加管理复杂度。 解决方案
  • 将 Refresh Token 存入 Redis,并设置一定的生命周期。
  • 限制 Refresh Token 的使用次数,每次刷新 JWT 后生成新的 Refresh Token。 总结

问题解决方案
JWT 无法主动撤销短生命周期 + Refresh Token + 黑名单
JWT 体积大只存必要数据,压缩优化
JWT 不能修改短生命周期,检查 Token 版本号
JWT 容易泄露使用 HTTPS,绑定 IP/设备 ID
JWT 不适用于所有场景适用于无状态认证,复杂场景考虑 Session
Refresh Token 需要存储使用 Redis 存储 Refresh Token

JWT 适合的场景

适合

  • 移动端 / Web 前后端分离项目
  • API 认证(比如 OAuth2)
  • 需要无状态认证的服务

不适合

  • 需要高安全性、随时撤销 Token(如银行、企业后台管理系统)
  • 需要频繁更新用户权限的系统”

六、token和JWT的区别

Token 是一种身份验证凭证,可以是任何格式的字符串。它用于标识用户身份或授权。 JWT(JSON Web Token) 是一种特定格式的 Token,由三部分组成:Header、Payload 和 Signature。JWT 自包含信息,并通过签名验证完整性,不需要服务器存储会话数据。

主要区别:

  • Token:广义术语,可以是任何形式的凭证。
  • JWT:一种基于 JSON 的 Token,具有固定结构和加密功能,适用于无状态应用。

七、参考文献

JSON Web Token 入门教程
jwt.io
jjwt

上次更新于: