JWT 认证
本教程共 47 篇 · 第 21 篇 · 更新于 2026-08-09 · 约 14 分钟阅读
本节目标:掌握 JWT 在 NestJS 中的完整用法,包括 Token 签发与验证、异步配置、刷新 Token 机制、Token 黑名单和登出实现。
JWT 是什么
上一节我们用 Passport 搭了一个基础的 JWT 认证。这一节把 JWT 的细节掰开揉碎讲清楚。
JWT(JSON Web Token)本质上就是一个加密的 JSON 对象。它由三部分组成,用 . 连接:
Header.Payload.Signature
- Header:声明 Token 类型和签名算法(如 HS256)
- Payload:存放的数据(比如用户 ID、角色)
- Signature:用 secret 对前两部分做签名,防止篡改
NoteJWT 的 Payload 部分只是 Base64 编码,不是加密。任何人都能解码看到里面的内容。所以不要往 JWT 里放密码、身份证号等敏感信息。
JWT 模块配置
基本配置
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
@Module({
imports: [
JwtModule.register({
secret: 'your-secret-key',
signOptions: { expiresIn: '1h' },
}),
],
})
export class AuthModule {}
异步配置(推荐)
生产环境中,secret 应该从环境变量读取:
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { ConfigModule, ConfigService } from '@nestjs/config';
@Module({
imports: [
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: {
expiresIn: configService.get<string>('JWT_EXPIRES_IN', '1h'),
},
}),
inject: [ConfigService],
}),
],
})
export class AuthModule {}
Tip异步配置的好处是 secret 不会硬编码在代码里。配合
.env文件使用,开发和生产环境可以用不同的密钥。
Token 签发
登录成功后,用 JwtService.sign() 生成 Token:
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private usersService: UsersService,
) {}
async login(email: string, password: string) {
const user = await this.usersService.findByEmail(email);
if (!user) {
throw new UnauthorizedException('用户不存在');
}
const payload = {
sub: user.id, // 用户 ID,JWT 标准字段
email: user.email,
roles: user.roles,
};
return {
access_token: this.jwtService.sign(payload),
user: {
id: user.id,
email: user.email,
name: user.name,
},
};
}
}
payload 里放什么由你决定,但有几个约定俗成的习惯:
sub:存放用户 ID(JWT 标准规定sub表示”主题”)- 不要放密码、手机号等敏感信息
- 不要放太多数据,Token 越小越好
WarningJWT 的 Payload 是明文编码的,任何人都能解码。只放”允许被看到”的信息。
你也可以在签发时覆盖过期时间:
const token = this.jwtService.sign(payload, {
expiresIn: '15m',
issuer: 'my-app',
audience: 'my-app-users',
});
Token 验证
通过 Passport 自动验证
上一节讲的 JWT 策略已经帮你做了验证——Passport 自动检查签名和过期时间。
手动验证
有时候你需要手动验证 Token(比如在自定义守卫里):
@Injectable()
export class AuthService {
constructor(private jwtService: JwtService) {}
async validateToken(token: string) {
try {
const payload = this.jwtService.verify(token);
return payload;
} catch (error) {
throw new UnauthorizedException('Token 无效或已过期');
}
}
}
verify() 和 decode() 的区别:
verify():验证签名 + 检查过期时间,不合法就抛异常decode():只解码,不验证。用于需要读取 Token 内容但不关心合法性的场景
// 只解码,不验证
const decoded = this.jwtService.decode(token);
console.log(decoded); // { sub: 1, email: '...', iat: ..., exp: ... }
刷新 Token 机制
JWT 有一个天然的问题:一旦签发,在过期之前无法作废。如果 Token 被盗,攻击者在过期之前都能用它。
解决方案是”双 Token 机制”:
- Access Token:短命(15 分钟),用于访问 API
- Refresh Token:长命(7 天),用于换取新的 Access Token
登录时签发两个 Token
@Injectable()
export class AuthService {
constructor(
private jwtService: JwtService,
private usersService: UsersService,
private refreshTokenService: RefreshTokenService,
) {}
async login(user: any) {
const payload = { sub: user.id, email: user.email };
const accessToken = this.jwtService.sign(payload, {
expiresIn: '15m',
});
const refreshToken = this.jwtService.sign(payload, {
expiresIn: '7d',
});
// 把 refresh token 存到数据库
await this.refreshTokenService.save(user.id, refreshToken);
return {
access_token: accessToken,
refresh_token: refreshToken,
};
}
}
刷新接口
客户端的 access_token 过期后,用 refresh_token 来换新的:
async refreshToken(refreshToken: string) {
// 1. 检查数据库里有没有这个 refresh token
const storedToken = await this.refreshTokenService.find(refreshToken);
if (!storedToken) {
throw new UnauthorizedException('无效的 refresh token');
}
// 2. 验证 token 是否过期
try {
const payload = this.jwtService.verify(refreshToken);
const user = await this.usersService.findOne(payload.sub);
// 3. 签发新的 access token
const newAccessToken = this.jwtService.sign(
{ sub: user.id, email: user.email },
{ expiresIn: '15m' },
);
return { access_token: newAccessToken };
} catch {
throw new UnauthorizedException('refresh token 已过期');
}
}
Refresh Token 存储
Refresh Token 需要存到数据库里,这样才能支持”撤销”和”登出所有设备”:
@Injectable()
export class RefreshTokenService {
constructor(
@InjectRepository(RefreshToken)
private tokenRepository: Repository<RefreshToken>,
) {}
async save(userId: number, token: string) {
const expiresAt = new Date();
expiresAt.setDate(expiresAt.getDate() + 7);
const refreshToken = this.tokenRepository.create({
userId,
token,
expiresAt,
});
return this.tokenRepository.save(refreshToken);
}
async find(token: string) {
return this.tokenRepository.findOne({ where: { token } });
}
async revoke(token: string) {
await this.tokenRepository.delete({ token });
}
async revokeAll(userId: number) {
await this.tokenRepository.delete({ userId });
}
}
Tip这里用了 TypeORM 的 Repository 做演示。不管你用什么数据库方案,核心思路一样:存 Token、查 Token、删 Token。
Token 黑名单
有时候你想让某个 Token 提前失效(比如用户修改了密码、或者主动登出)。JWT 本身不支持”作废”,但你可以维护一个黑名单。
@Injectable()
export class TokenBlacklistService {
constructor(
@InjectRepository(BlacklistedToken)
private blacklistRepository: Repository<BlacklistedToken>,
) {}
async addToBlacklist(token: string, expiresAt: Date) {
const blacklisted = this.blacklistRepository.create({
token,
expiresAt,
});
await this.blacklistRepository.save(blacklisted);
}
async isBlacklisted(token: string): Promise<boolean> {
const count = await this.blacklistRepository.count({
where: { token },
});
return count > 0;
}
// 定期清理过期的黑名单记录
async cleanupExpired() {
await this.blacklistRepository
.createQueryBuilder()
.delete()
.where('expiresAt < :now', { now: new Date() })
.execute();
}
}
然后在守卫里检查黑名单:
@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
constructor(
private reflector: Reflector,
private blacklistService: TokenBlacklistService,
) {
super();
}
async canActivate(context: ExecutionContext) {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization?.split(' ')[1];
if (token && await this.blacklistService.isBlacklisted(token)) {
throw new UnauthorizedException('Token 已被撤销');
}
return super.canActivate(context) as Promise<boolean>;
}
}
Note黑名单方案在生产环境中通常用 Redis 来实现,性能比数据库好很多。Token 过期后自动从 Redis 中删除,不需要手动清理。
登出实现
有了黑名单机制,登出就是把当前 Token 加进黑名单:
@Controller('auth')
export class AuthController {
constructor(
private authService: AuthService,
private blacklistService: TokenBlacklistService,
private jwtService: JwtService,
) {}
@Post('logout')
@UseGuards(JwtAuthGuard)
async logout(@Request() req) {
const token = req.headers.authorization?.split(' ')[1];
const decoded = this.jwtService.decode(token) as any;
// 把 Token 加入黑名单,过期时间跟 Token 一致
await this.blacklistService.addToBlacklist(
token,
new Date(decoded.exp * 1000),
);
return { message: '登出成功' };
}
}
Tip
decoded.exp是 Unix 时间戳(秒),需要乘以 1000 转成 JavaScript 的毫秒时间戳。
JWT 最佳实践
1. 用环境变量管理密钥
# .env
JWT_SECRET=your-super-secret-key-at-least-32-characters-long
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
生成一个足够长的随机密钥:
import * as crypto from 'crypto';
const secret = crypto.randomBytes(64).toString('hex');
2. Access Token 过期时间要短
15 分钟是比较合理的默认值。配合 Refresh Token 机制,用户体验不会受影响,但安全性大幅提升。
3. 不要在 Payload 里放敏感信息
// 错误做法
const payload = {
sub: user.id,
password: user.password, // 绝对不行
creditCard: user.creditCard, // 绝对不行
};
// 正确做法
const payload = {
sub: user.id,
email: user.email,
};
4. 生产环境必须用 HTTPS
JWT 在请求头里传输,如果是 HTTP,Token 会被明文截获。
5. 设置全局 JWT 模块
如果你的项目到处都需要 JwtService,可以设置 global: true 避免到处 import:
JwtModule.register({
global: true,
secret: jwtConstants.secret,
signOptions: { expiresIn: '60s' },
}),
小结
JWT 认证的核心流程:
- 登录时验证身份,签发 Token
- 后续请求携带 Token,服务端验证签名和有效期
- Token 快过期时用 Refresh Token 换取新 Token
- 需要主动作废时用黑名单机制
关键知识点回顾:
JwtModule.register()/registerAsync()配置签发参数JwtService.sign()签发 Token,verify()验证,decode()只解码- Payload 是明文的,不要放敏感信息
- 双 Token 机制(Access + Refresh)兼顾安全和体验
- Token 黑名单解决 JWT 无法主动作废的问题
- 密钥必须用环境变量管理,不能硬编码
下一章我们聊聊权限控制,解决”你能干啥”的问题。