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: '...',
}