返回文章列表 →

Note

JWT

令牌,一种token格式

发布于 更新于

JWT

JSON Web Token,一种传递信息的令牌格式,即本质是token。使用 JWT 格式生成 Access Token,并通过 Bearer 方式传输。

作用

在用户登录成功后,为了避免用户后续每次请求都要提交密码,后端生成并返回 JWT 给客户端,客户端后续访问受保护接口时携带它。后端验证 JWT 后,就可以确认当前请求属于哪个用户。

安装和导入

// 安装
npm install @nestjs/jwt

// 引入
import { JwtModule } from '@nestjs/jwt';

JWT的结构

由三部分组成:Header.Payload.Signature

1.Header:头部

可能是:

{
  "alg": "HS256", // 表示使用 HMAC SHA-256 算法生成签名
  "typ": "JWT",
}
字段 含义
alg 使用什么算法生成签名
typ 令牌类型,通常是 JWT
JWT的签名算法
  • 对称算法:HS256 用同一个密钥签发和验证 JWT
  • 非对称算法:RS256 私钥签发,公钥验证

2.Payload:载荷

Payload 是 JWT 中真正携带的数据,里面每一组名称和值都叫作 Claim,即“声明”,Payload不要存放敏感信息。例如:

{
  "sub": "1001",
  "username": "xiaoming",
  "role": "user",
  "iat": 1785390000,
  "exp": 1785390900,
}
JWT中常见的Claim
Claim 完整名称 作用
sub Subject JWT 代表的主体,认证中通常放用户 ID
iss Issuer JWT 的签发者
aud Audience JWT 的预期接收者
iat Issued At JWT 的签发时间
exp Expiration Time JWT 的过期时间。类型为 Unix 秒时间戳
nbf Not Before 在此时间之前不能使用
jti JWT ID JWT 的唯一编号

Payload 本质上是一个对象,除了上面的 JWT 标准 Claim,还可以添加业务自定义 Claim,例如 username、role、permissions。属性名由应用自己定义,但不要存放密码、密钥、身份证号等敏感信息。

// 标准 Claim + 自定义 Claim
const payload = {
  sub: "1001",
  username: "xiaoming",
  role: "user",
};

3. Signature:签名

对于 HS256,可以概念性地理解为: Signature = HS256(编码后的 Header + 编码后的 Payload + JWT_SECRET) 。 JWT_SECRET 为JWT密钥。服务器收到 JWT 后,会使用相同的密钥重新计算签名,如果结果不同,表明JWT 被篡改或不是本服务器签发。所以即使 Payload 可以看见,却不能随意修改,因为修改 Payload 后,原来的签名就不匹配了

JWT模块的注册

有同步/异步的方式注册JWT 模块,下面是异步使用 ConfigService 注册 auth.module.ts

import { Module } from "@nestjs/common";
import { ConfigModule, ConfigService } from "@nestjs/config";
import { JwtModule } from "@nestjs/jwt";
import { AuthService } from "./auth.service";

@Module({
  imports: [
    JwtModule.registerAsync({
      // JWT 模块配置

      // 导入ConfigModule。如果AppModule已将ConfigModule设置为全局模块,这里可以省略imports
      imports: [ConfigModule],

      // 注入ConfigService
      inject: [ConfigService],

      // 工厂函数返回JWT配置
      useFactory: (configService: ConfigService) => ({
        // 从环境变量中获取JWT密钥
        secret: configService.getOrThrow<string>("JWT_SECRET"),
        // 签名配置
        signOptions: {
          algorithm: "HS256",
          expiresIn: 15 * 60,
          issuer: "my-nest-api",
          audience: "my-web-app",
        },
        // 验证配置
        verifyOptions: {
          algorithms: ["HS256"],
          issuer: "my-nest-api",
          audience: "my-web-app",
        },
      }),
    }),
  ],

  providers: [AuthService],

  exports: [JwtModule, AuthService],
})
export class AuthModule {}

signOptions 属性列表

属性 类型 作用
algorithm Algorithm 签名算法,默认为 HS256
keyid string 设置 JWT Header 的 kid
expiresIn number | StringValue 有效时长,数字单位为秒,并据此生成 Payload 的 exp
notBefore number | StringValue 延迟生效时长,并据此生成 Payload 的 nbf
audience string | string[] 生成 Payload 的 aud
subject string 生成 Payload 的 sub
issuer string 生成 Payload 的 iss
jwtid string 生成 Payload 的 jti
mutatePayload boolean 是否直接修改传入的 payload 对象,默认不需要开启
noTimestamp boolean 是否不自动生成 iat
header JwtHeader 补充或覆盖 JWT Header
encoding string 字符串 payload 的编码,默认为 utf8
allowInsecureKeySizes boolean 是否允许使用低于 2048 位的 RSA 私钥,通常不应开启
allowInvalidAsymmetricKeyTypes boolean 是否允许算法与非对称密钥类型不匹配,只用于向后兼容,通常不应开启

verifyOptions 属性列表

属性 类型 作用
algorithms Algorithm[] 允许的签名算法白名单,例如 ['HS256']
audience string | RegExp | Array<string | RegExp> 期望的 aud Claim,不匹配则验证失败
issuer string | string[] 期望的 iss Claim,不匹配则验证失败
subject string 期望的 sub Claim,不匹配则验证失败
jwtid string 期望的 jti Claim,不匹配则验证失败
nonce string 期望的 nonce Claim,主要用于 OpenID Connect ID Token
clockTimestamp number 指定验证时作为“当前时间”的 Unix 秒级时间戳
clockTolerance number 验证 nbf 和 exp 时允许的时钟误差,单位为秒
maxAge number | string 以 iat 为基准限制 Token 的最大存活时间
ignoreExpiration boolean 是否忽略 exp 过期检查,通常不应开启
ignoreNotBefore boolean 是否忽略 nbf 生效时间检查,通常不应开启
complete boolean 是否返回 { header, payload, signature } 完整结构,默认只返回 payload
allowInvalidAsymmetricKeyTypes boolean 是否允许算法与非对称密钥类型不匹配,通常不应开启

signOptions 中是单数 algorithm,verifyOptions 中是复数 algorithms。verifyOptions 没有 notBefore,如果 Token 含有 nbf,默认会自动检查;只有 ignoreNotBefore 可以关闭该检查。

JwtModule 中的 signOptions 和 verifyOptions 是模块默认配置。调用 sign() / signAsync() / verify() / verifyAsync() 时传入的 options 是本次调用配置,同名属性会覆盖模块默认值。

为什么 expiresIn 传普通 string 可能报错

expiresIn 和 notBefore 在当前类型定义中不是任意 string,而是 number | StringValue。"15m"、"1h"这样的字面量符合 StringValue,但从环境变量读出的值通常被 TypeScript 推断为更宽泛的 string,因此可能报类型错误。

如果环境变量保存的是秒数,可以显式转换成数字:

signOptions: {
  expiresIn: Number(
    configService.getOrThrow<string>("JWT_EXPIRES_IN"),
  ),
}

getOrThrow<number>() 中的泛型只是告诉 TypeScript 期望的类型,不会把 .env 读取到的字符串自动转换为数字,所以仍建议执行 Number() 转换并在配置加载阶段校验结果。

JwtService的引入

import { Injectable } from "@nestjs/common";
import { JwtService } from "@nestjs/jwt";

@Injectable()
export class AuthService {
  constructor(private readonly jwtService: JwtService) {}
}

常用api

通过 JwtService 来访问api,如生成jwt JwtService.sign() 。

sign()

生成 JWT 。

方法 作用 返回值
sign() 同步生成 JWT string
signAsync() 异步生成 JWT Promise<string>

参数和返回值

signAsync(
  payload: string | object | Buffer,
  options?: JwtSignOptions,
): Promise<string>
参数 含义
payload JWT 载荷,建议使用对象,属性可以自定义
options 此次生成 JWT 的配置,属性名受 JwtSignOptions 限制

常用options

选项 作用 影响的 JWT 字段
expiresIn 设置有效时长。数字单位为秒;字符串可使用 '15m'、'1h'、'7d' Payload exp
notBefore 设置相对于签发基准时间多久后生效 Payload nbf
issuer 设置签发者 Payload iss
audience 设置预期接收者 Payload aud
subject 设置 Token 主体 Payload sub
jwtid 设置 Token 唯一编号 Payload jti
algorithm 指定签名算法,例如 'HS256' Header alg
keyid 设置密钥标识 Header kid

❗expiresIn、notBefore、audience、issuer、subject、jwtid 是生成对应标准 Claim 的便捷选项。对应的 exp、nbf、aud、iss、sub、jti 不能同时出现在 payload 中,否则签名时会报错。自定义 Claim 只放在 payload 中。

signAsync() 的 options 使用上面相同的签名选项,Nest 的 JwtSignOptions 还额外允许使用 secret 或 privateKey 覆盖本次签名密钥。它们不属于 payload,也不是 JwtModule.signOptions 的属性。

示例

const token = await this.jwtService.signAsync(
  {
    // Payload:可以放自定义 Claim
    sub: "1001",
    username: "xiaoming",
    role: "user",
  },
  {
    // Options:只能使用 JwtSignOptions 已定义的属性
    expiresIn: "15m",
  },
);

verify()

验证 JWT

方法 作用 返回值
verify() 同步验证 JWT Payload,失败时抛异常
verifyAsync() 异步验证 JWT Promise<Payload>

参数和返回值

verifyAsync<
  T extends object = any
>(
  token: string,
  options?: JwtVerifyOptions,
): Promise<T>
参数 含义
token 完整的JWT
options 此次验证 JWT 使用的额外规则
常用options
选项 含义
algorithms 允许的签名算法白名单。例如 algorithms: ['HS256']
issuer 期望 Payload 的 iss 等于该值
audience 期望 Payload 的 aud 匹配该值
subject 期望 Payload 的 sub 等于该值
jwtid 期望 Payload 的 jti 等于该值
ignoreExpiration 是否忽略 exp 过期检查,通常不应开启
ignoreNotBefore 是否忽略 nbf 生效时间检查,通常不应开启
clockTolerance 检查 exp 和 nbf 时允许的时钟误差,单位为秒
maxAge 从 iat 起计算的最大存活时间,例如 '15m'
complete 默认只返回 payload;开启后返回 { header, payload, signature }

verifyAsync() 的 options 使用上面相同的验证选项,Nest 的 JwtVerifyOptions 还额外允许使用 secret 或 publicKey 覆盖本次验证密钥。

verify() 始终验证签名。Token 含有 exp 和 nbf 时默认检查过期时间和生效时间;issuer、audience、subject、jwtid 等只有在 verifyOptions 中声明期望值时才会额外校验。

verifyAsync<PayloadType>() 的泛型只用于 TypeScript 类型提示,不会在运行时自动验证 payload 是否真的符合 PayloadType。对于不可完全信任的 Token,仍需要校验业务 Claim 的类型和取值。

verify 的错误处理

verify() 验证失败会直接抛出异常,常见错误包括:

错误 含义
TokenExpiredError JWT 已过期
JsonWebTokenError JWT 格式、签名、issuer 等验证失败
NotBeforeError 还未到 JWT 的生效时间

示例

try {
  const payload =
    await this.jwtService.verifyAsync<VerifiedAccessTokenPayload>(token);

  return payload;
} catch {
  throw new UnauthorizedException("登录凭证无效或已过期");
}

decode()

解析 JWT

方法 作用 返回值
decode() 解码 JWT,读取里面的 Header 和 Payload 解码内容或 null

参数和返回值

decode(
  token: string,
  options?: DecodeOptions,
)
参数 含义
token 完整的JWT
options 控制解码返回形式,不会进行签名或 Claim 验证

DecodeOptions 只有两个属性:

选项 含义
complete 是否返回 { header, payload, signature } 完整结构
json 是否强制将 payload 按 JSON 解析,即使 Header 的 typ 不是 JWT

❗decode() 只是 Base64URL 解码,不验证签名、有效期、签发者或接收者。不能相信 decode() 的结果,身份认证必须使用 verify() 或 verifyAsync()。

示例

const payload =
  this.jwtService.decode(token);

// 可能得到
  {
  sub: '1001',
  username: 'xiaoming',
  iat: 1785390000,
  exp: 1785390900,
}


// 查看完整内容
const result =
  this.jwtService.decode(token, {
    complete: true,
  });

//   可能得到
{
  header: {
    alg: 'HS256',
    typ: 'JWT',
  },

  payload: {
    sub: '1001',
    username: 'xiaoming',
  },

  signature: '...',
}

参考资料