jsonwebtoken 教程

jsonwebtoken 提供 JWT 的签发、验证和解码能力。本章重点学习 sign()verify()decode()、过期时间和算法限制。

1. 安装与导入

npm install jsonwebtoken
npm install --save-dev @types/jsonwebtoken

当前项目启用了 esModuleInterop,可以写:

import jwt from "jsonwebtoken";

TypeScript 会根据当前 module: "commonjs" 配置转换模块,仍可使用:

ts-node server.ts

2. 准备 Secret

HS256 使用共享 Secret。不要把 Secret 写进 Git 仓库:

JWT_ACCESS_SECRET=请替换为足够长的随机值

统一读取并尽早检查:

function readRequiredEnv(name: string): string {
  const value = process.env[name];

  if (!value) {
    throw new Error(`缺少环境变量:${name}`);
  }

  return value;
}

const accessTokenSecret = readRequiredEnv("JWT_ACCESS_SECRET");

生产环境应由密钥管理服务或部署平台注入 Secret。Secret 应具有足够随机性,不能使用 123456、项目名等可猜值。

3. jwt.sign():生成 Token

基础签名:

const token = jwt.sign(
  { role: "user" },
  accessTokenSecret,
  {
    algorithm: "HS256",
    subject: "user-123",
    issuer: "nloop-api",
    audience: "nloop-client",
    expiresIn: "15m",
  }
);

三个参数分别是:

  1. payload:要携带的声明。
  2. secretOrPrivateKey:HMAC Secret 或非对称算法私钥。
  3. options:算法、有效期、主体、签发方等设置。

默认会添加 iat。官方文档说明 expiresInissueraudience 等没有默认值,应主动配置需要的约束。

不要同时在 Payload 和 options 中重复设置同一声明。例如不要同时使用:

// 错误思路:exp 与 expiresIn 重复
jwt.sign(
  { exp: 1234567890 },
  accessTokenSecret,
  { expiresIn: "15m" }
);

4. expiresIn:设置有效期

支持数字或时间字符串:

jwt.sign(payload, secret, { expiresIn: 900 });   // 900 秒
jwt.sign(payload, secret, { expiresIn: "15m" }); // 15 分钟
jwt.sign(payload, secret, { expiresIn: "7d" });  // 7 天

一个容易踩坑的细节:

expiresIn: 120    // 数字:120 秒
expiresIn: "120"  // 无单位字符串:由 ms 语法按 120 毫秒解析

因此字符串一定写单位,例如 "120s""15m""7d"

5. algorithm:签发时选择算法

algorithm 是单数,因为签发一个 Token 时只选择一个算法:

const token = jwt.sign(payload, accessTokenSecret, {
  algorithm: "HS256",
  expiresIn: "15m",
});

若不指定,jsonwebtoken 默认使用 HS256。教程仍显式声明它,让配置和安全边界更加清楚。

使用 RS256 时应传入 RSA 私钥,并使用至少 2048 位的 RSA 密钥。不要启用官方文档中仅为兼容旧系统提供的 allowInsecureKeySizesallowInvalidAsymmetricKeyTypes

6. jwt.verify():验证 Token

const decoded = jwt.verify(token, accessTokenSecret, {
  algorithms: ["HS256"],
  issuer: "nloop-api",
  audience: "nloop-client",
});

同步调用成功时返回 Payload,失败时抛出异常。它会根据配置验证:

为什么要设置 algorithms

algorithms 是复数,因为验证方可能允许一个算法列表:

algorithms: ["HS256"]

即使库能根据密钥类型推导默认算法,也建议显式设置白名单,避免接受业务系统未计划使用的算法。

验证后的类型收窄

verify() 的结果仍是外部输入。不要直接通过类型断言宣称它一定符合业务类型:

interface AccessTokenPayload {
  sub: string;
  tokenType: "access";
  role: "user" | "admin";
}

function isAccessTokenPayload(
  value: string | jwt.JwtPayload
): value is jwt.JwtPayload & AccessTokenPayload {
  return (
    typeof value !== "string" &&
    typeof value.sub === "string" &&
    value.tokenType === "access" &&
    (value.role === "user" || value.role === "admin")
  );
}

const decoded = jwt.verify(token, accessTokenSecret, {
  algorithms: ["HS256"],
  issuer: "nloop-api",
  audience: "nloop-client",
});

if (!isAccessTokenPayload(decoded)) {
  throw new Error("Token Payload 格式不正确");
}

console.log(decoded.sub);

7. 处理验证错误

try {
  const decoded = jwt.verify(token, accessTokenSecret, {
    algorithms: ["HS256"],
  });

  console.log(decoded);
} catch (error: unknown) {
  if (error instanceof jwt.TokenExpiredError) {
    console.log("Token 已过期", error.expiredAt);
  } else if (error instanceof jwt.NotBeforeError) {
    console.log("Token 尚未生效", error.date);
  } else if (error instanceof jwt.JsonWebTokenError) {
    console.log("Token 格式或签名不正确");
  } else {
    throw error;
  }
}

对外通常返回统一的 401 Unauthorized,不必向客户端暴露具体签名失败原因;详细原因可在不包含完整 Token 的内部日志中记录。

8. jwt.decode():只读取,不验证

const decoded = jwt.decode(token);

decode() 不验证签名,不应判断登录状态或权限。适合的用途包括:

查看完整结构:

const decoded = jwt.decode(token, { complete: true });

if (decoded) {
  console.log(decoded.header);
  console.log(decoded.payload);
  console.log(decoded.signature);
}

错误示例:

// 严禁:攻击者可以伪造 role
const decoded = jwt.decode(token) as { role: string };

if (decoded.role === "admin") {
  // 执行管理员操作
}

9. 同步与异步形式

传入 callback 时使用异步形式:

jwt.sign(payload, secret, options, (error, token) => {
  // ...
});

不传 callback 时是同步形式并直接返回或抛错。JWT 签名验证通常是纯计算;初学案例选择同步形式,流程更容易阅读。使用远程 JWKS 获取公钥时,verify() 的异步形式更有意义。

10. 本章结论

sign      生成带签名的 Token
verify    验证签名和声明,认证时必须使用
decode    只读取内容,不能建立信任
algorithm 签发时选择一个算法
algorithms 验证时限制允许的算法列表
expiresIn 设置有效期,字符串要明确单位