NestJS 守卫使用场景
JWT 身份认证、角色授权(RBAC)、细粒度权限控制、多策略认证授权、IP 白名单守卫、接口访问频率限制
JWT 身份认证
验证请求发起者是否为合法用户(如登录态校验),替代传统中间件认证的优势是:守卫可通过 ExecutionContext 精准获取请求、路由、用户等上下文信息,且支持依赖注入(如注入认证服务)。
- 定义守卫
ts
// jwt-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
// 获取请求对象(支持 HTTP/WSS/RPC 等上下文)
const request = context.switchToHttp().getRequest<Request>();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException('未提供认证 Token');
}
try {
// 验证 Token 并解析用户信息
const payload = await this.jwtService.verifyAsync(token, {
secret: process.env.JWT_SECRET,
});
// 将用户信息挂载到请求对象,供后续处理器使用
request.user = payload;
} catch {
throw new UnauthorizedException('Token 无效或已过期');
}
return true;
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}- 使用守卫
ts
// 控制器
@Controller('users')
export class UsersController {
// 局部守卫:仅该接口需要认证
@Get('profile')
@UseGuards(JwtAuthGuard)
getProfile(@Request() req) {
return req.user;
}
}
// 全局守卫:所有接口默认需要认证(可通过装饰器排除)
// app.module.ts
providers: [
{
provide: APP_GUARD,
useClass: JwtAuthGuard,
},
];角色授权(RBAC)
最常用的场景:限制特定角色(如管理员、普通用户)访问接口,结合自定义装饰器 + 守卫实现。
- 定义角色装饰器
ts
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);- 实现角色守卫
ts
// roles.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { ROLES_KEY } from './roles.decorator';
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
// 获取路由上的角色元数据
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(), // 优先获取方法级元数据
context.getClass(), // 其次获取控制器级元数据
]);
// 无角色限制时直接放行
if (!requiredRoles) return true;
// 获取已认证的用户信息
const request = context.switchToHttp().getRequest();
const user = request.user;
// 校验用户角色是否匹配
const hasRole = requiredRoles.includes(user.role);
if (!hasRole) {
throw new ForbiddenException('无权限访问(需要角色:' + requiredRoles.join(',') + ')');
}
return hasRole;
}
}- 使用守卫
ts
@Controller('admin')
export class AdminController {
// 仅管理员可访问
@Post('create-user')
@UseGuards(JwtAuthGuard, RolesGuard) // 先认证,再鉴权
@Roles('admin')
createUser() {
return '创建用户成功';
}
// 管理员/运营均可访问
@Get('logs')
@UseGuards(JwtAuthGuard, RolesGuard)
@Roles('admin', 'operator')
getLogs() {
return '操作日志列表';
}
}细粒度权限控制
资源所有权 、操作权限
- 定义守卫
ts
// 示例:验证文章所有权
// 比角色更细的控制:比如 “仅文章作者可编辑自己的文章”、“用户只能修改自己的密码”、“基于权限码(如 article:edit)控制操作”。
// ownership.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
import { ArticlesService } from './articles.service';
@Injectable()
export class ArticleOwnershipGuard implements CanActivate {
constructor(private articlesService: ArticlesService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const userId = request.user.id; // 已认证用户ID
const articleId = request.params.id; // 要操作的文章ID
// 校验文章是否属于当前用户
const article = await this.articlesService.findOne(articleId);
if (!article || article.authorId !== userId) {
throw new ForbiddenException('无权操作他人的文章');
}
return true;
}
}- 使用守卫
ts
@Put('articles/:id')
@UseGuards(JwtAuthGuard, ArticleOwnershipGuard)
updateArticle(@Param('id') id: string, @Body() data) {
return this.articlesService.update(id, data);
}多策略认证授权
同一接口支持多种认证方式(如 JWT + OAuth2 + API Key),或根据不同条件切换授权逻辑。
ts
// multi-strategy.guard.ts
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { ApiKeyService } from './api-key.service';
@Injectable()
export class MultiStrategyGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private apiKeyService: ApiKeyService
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
// 尝试 JWT 认证
try {
const token = request.headers.authorization?.split(' ')[1];
if (token) {
const payload = await this.jwtService.verifyAsync(token);
request.user = payload;
return true;
}
} catch {}
// JWT 失败,尝试 API Key 认证
const apiKey = request.headers['x-api-key'];
if (apiKey && (await this.apiKeyService.validate(apiKey))) {
request.user = await this.apiKeyService.getUserByApiKey(apiKey);
return true;
}
// 所有策略失败
throw new UnauthorizedException('请通过 JWT 或 API Key 认证');
}
}IP 白名单守卫
ts
// ip-whitelist.guard.ts
import { CanActivate, ExecutionContext, Injectable, ForbiddenException } from '@nestjs/common';
@Injectable()
export class IpWhitelistGuard implements CanActivate {
// 白名单 IP 列表(可从配置/数据库读取)
private readonly whitelist = ['127.0.0.1', '192.168.1.100'];
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const clientIp = request.ip || request.ips[0]; // 获取客户端IP
if (!this.whitelist.includes(clientIp)) {
throw new ForbiddenException('IP 不在白名单内,禁止访问');
}
return true;
}
}接口访问频率限制
ts
// src/guards/rate-limit.guard.ts
import {
Injectable,
CanActivate,
ExecutionContext,
TooManyRequestsException,
} from '@nestjs/common';
// 模拟存储 IP 访问次数(生产环境用 Redis)
const ipAccessMap = new Map<string, { count: number; lastTime: number }>();
@Injectable()
export class RateLimitGuard implements CanActivate {
// 限制:1 分钟内最多 10 次请求
private readonly limit = 10;
private readonly window = 60 * 1000;
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const ip = request.ip;
const now = Date.now();
// 初始化/更新 IP 访问记录
const record = ipAccessMap.get(ip) || { count: 0, lastTime: now };
if (now - record.lastTime > this.window) {
record.count = 1;
record.lastTime = now;
} else {
record.count += 1;
}
ipAccessMap.set(ip, record);
// 校验频率
if (record.count > this.limit) {
throw new TooManyRequestsException('请求过于频繁,请稍后再试');
}
return true;
}
}