1-7 进阶 测试 JWT 模块:应用守卫 AuthGuard、JwtService 签名 Payload
本节将前面学习的所有概念串联起来,实现完整的 JWT 认证流程:从用户登录签发 Token,到使用 AuthGuard 保护路由,再到携带 Token 访问受保护接口的全流程测试验证。
JWT 认证流程总览
阶段一:登录签发 Token
═══════════════════════
客户端 POST /auth/signin { username, password }
│
▼ ValidationPipe + DTO 校验参数
│
▼ AuthController(公开接口)
│
▼ AuthService.signIn()
│ ├── UserRepository.findByUsername() → 查询用户
│ ├── 校验密码
│ └── JwtService.signAsync({ sub, username }) → 签发 Token
│
▼
响应 { access_token: "eyJhbG..." }
阶段二:访问受保护接口
═══════════════════════
客户端 GET /auth/profile
Headers: Authorization: Bearer eyJhbG...
│
▼ AuthGuard('jwt') 拦截
│
▼ Passport + passport-jwt 自动验证:
│ 1. 从 Authorization Header 提取 Bearer Token
│ 2. 使用 secret 验证签名合法性
│ 3. 检查 Token 是否过期
│ 4. 解码 payload
│
▼ JwtStrategy.validate(payload)
│ └── return { userId: payload.sub, username: payload.username }
│
▼ req.user = validate() 返回值
│
▼ Controller.getProfile()
└── return req.user
text
validate() 方法详解
validate() 方法由 @nestjs/passport 框架在 Token 验证通过后自动调用:
// auth/strategy/jwt.strategy.ts
import { Injectable } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy) {
constructor(protected configService: ConfigService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>('JWT_SECRET'),
});
}
/**
* Passport 自动完成以下步骤后调用此方法:
* 1. 验证 JWT 签名的合法性(使用 secretOrKey)
* 2. 检查 Token 是否过期(ignoreExpiration: false)
* 3. 解码 JWT 得到 Payload 对象
*
* @param payload - 解码后的 JWT Payload
* @returns 返回值会被挂载到 request.user
*/
async validate(payload: { sub: number; username: string }) {
return { userId: payload.sub, username: payload.username };
}
}
typescript
Payload 结构解析:
// JWT Token 中编码的 Payload
{
sub: 1, // 用户 ID(签发时自定义放入)
username: "john", // 用户名(签发时自定义放入)
iat: 1684281600, // Issued At:签发时间(自动添加)
exp: 1684368000 // Expiration:过期时间(自动添加)
}
typescript
| 字段 | 来源 | 说明 |
|---|---|---|
sub | 开发者签发时放入 | Subject,JWT 标准中用于标识主体 |
username | 开发者签发时放入 | 自定义字段 |
iat | JWT 库自动添加 | Token 签发时间戳 |
exp | JWT 库自动添加 | Token 过期时间戳 |
JwtService 签名 Payload
在 AuthService 中注入 JwtService,使用 signAsync() 签发 Token:
// auth/auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { UserRepository } from '../user/user.repository';
@Injectable()
export class AuthService {
constructor(
private readonly userRepository: UserRepository,
private readonly jwtService: JwtService,
) {}
async signIn(username: string, password: string) {
// 1. 查询用户
const user = await this.userRepository.findByUsername(username);
if (!user) {
throw new UnauthorizedException('用户名或密码错误');
}
// 2. 校验密码(实际项目中应使用 bcrypt 比对哈希)
if (user.password !== password) {
throw new UnauthorizedException('用户名或密码错误');
}
// 3. 签发 JWT Token
const payload = { sub: user.id, username: user.username };
return {
access_token: await this.jwtService.signAsync(payload),
};
}
}
typescript
JwtService 核心方法:
| 方法 | 用途 | 返回值 |
|---|---|---|
signAsync(payload) | 异步签发 Token | Promise<string> |
sign(payload) | 同步签发 Token | string |
verifyAsync(token) | 异步验证 Token | Promise<object> |
decode(token) | 解码 Token(不验证签名) | object | string |
推荐使用
signAsync()和verifyAsync()异步版本,避免阻塞事件循环。
自定义签名选项:
// 覆盖模块级默认配置
this.jwtService.signAsync(payload, {
expiresIn: '7d', // 覆盖过期时间
issuer: 'my-app', // 签发者
audience: 'my-users', // 受众
});
typescript
AuthGuard 守卫的三种应用方式
方式一:路由级别(单个接口)
import { Controller, Get, UseGuards, Request } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
@Controller('auth')
export class AuthController {
@UseGuards(AuthGuard('jwt'))
@Get('profile')
getProfile(@Request() req) {
return req.user; // validate() 返回的用户信息
}
}
typescript
方式二:Controller 级别(整个控制器)
@Controller('users')
@UseGuards(AuthGuard('jwt'))
export class UsersController {
@Get()
findAll() {
// 所有方法都需要 JWT 认证
}
}
typescript
方式三:全局注册(推荐)
// app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport';
@Module({
providers: [
{
provide: APP_GUARD,
useClass: AuthGuard('jwt'),
},
],
})
export class AppModule {}
typescript
三种方式对比:
| 方式 | 作用范围 | 适用场景 |
|---|---|---|
| 路由级别 | 单个方法 | 少量接口需要保护 |
| Controller 级别 | 整个 Controller | 模块内所有接口都需要保护 |
| 全局注册 | 所有路由 | 大部分接口都需要保护,公开接口用 @Public() 排除 |
自定义 AuthGuard + @Public() 装饰器
全局注册后,需要一种机制将登录、注册等公开接口排除在认证之外:
// auth/decorators/public.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const IS_PUBLIC_KEY = 'isPublic';
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
typescript
// auth/guards/auth.guard.ts
import {
CanActivate,
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
import { IS_PUBLIC_KEY } from '../decorators/public.decorator';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private reflector: Reflector,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 检查是否标记为公开接口
const isPublic = this.reflector.getAllAndOverride<boolean>(IS_PUBLIC_KEY, [
context.getHandler(),
context.getClass(),
]);
if (isPublic) return true;
// 从 Authorization Header 提取 Token
const request = context.switchToHttp().getRequest<Request>();
const [type, token] = request.headers.authorization?.split(' ') ?? [];
if (type !== 'Bearer' || !token) {
throw new UnauthorizedException();
}
// 验证 Token
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
request['user'] = payload;
} catch {
throw new UnauthorizedException();
}
return true;
}
}
typescript
在 Controller 中使用 @Public() 标记公开接口:
@Controller('auth')
export class AuthController {
@Public() // 无需认证
@Post('signin')
signIn(@Body() dto: SignInUserDto) {
return this.authService.signIn(dto.username, dto.password);
}
@Public() // 无需认证
@Post('signup')
signUp(@Body() dto: SignInUserDto) {
return this.authService.signUp(dto.username, dto.password);
}
@Get('profile') // 需要认证
getProfile(@Request() req) {
return req.user;
}
}
typescript
完整测试流程
测试 1:未认证访问受保护接口(应返回 401)
curl http://localhost:3030/api/v1/auth/profile
# 响应:
# {"statusCode":401,"message":"Unauthorized"}
bash
测试 2:登录获取 Token
curl -X POST http://localhost:3030/api/v1/auth/signin \
-H "Content-Type: application/json" \
-d '{"username": "testuser", "password": "123456"}'
# 响应:
# {"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."}
bash
测试 3:携带 Token 访问受保护接口
curl http://localhost:3030/api/v1/auth/profile \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
# 响应:
# {"userId": 1, "username": "testuser"}
bash
测试 4:Token 过期后访问(应返回 401)
# 等待 Token 过期后再次请求
curl http://localhost:3030/api/v1/auth/profile \
-H "Authorization: Bearer eyJhbGci..."
# 响应:
# {"statusCode":401,"message":"Unauthorized"}
bash
JwtModule 完整配置
// auth/auth.module.ts
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { JwtStrategy } from './strategy/jwt.strategy';
import { UserModule } from '../user/user.module';
@Module({
imports: [
UserModule,
PassportModule,
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: (configService: ConfigService) => ({
secret: configService.get<string>('JWT_SECRET'),
signOptions: { expiresIn: '1d' },
}),
inject: [ConfigService],
}),
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
exports: [AuthService],
})
export class AuthModule {}
typescript
本节要点
- JWT 签发:
JwtService.signAsync(payload)使用服务器密钥对用户信息签名,生成access_token - Payload 设计:使用
sub存储用户 ID(遵循 JWT 标准),username存储用户名 - AuthGuard 三种应用:路由级别、Controller 级别、全局注册,推荐全局注册 +
@Public()排除公开接口 - @Public() 装饰器:通过
SetMetadata标记公开路由,自定义 Guard 中通过Reflector读取元数据跳过认证 - Token 传递规范:必须放在
Authorization: Bearer <token>请求头中 - 自动验证机制:Passport + passport-jwt 自动完成 Token 提取、签名验证、过期检查,开发者只需在
validate()中处理解码后的 payload
↑