什么是守卫?🤔
守卫(Guard)是 NestJS 中用于处理授权的特殊类,它们决定请求是否应该由路由处理程序处理。守卫在中间件之后、拦截器或管道之前执行。
守卫最重要的一个特点是,它们可以访问 ExecutionContext 实例,因此确切地知道接下来要执行什么。它们的设计与异常过滤器、管道和拦截器非常相似,但又各有特色。
Express vs NestJS 对比 🆚
让我们看看在传统 Express 和 NestJS 中如何处理认证授权:
Express 写法:
javascript
// 认证中间件
const authMiddleware = (req, res, next) => {
const token = req.headers.authorization;
if (!token) {
return res.status(401).json({ message: '未提供令牌' });
}
try {
const decoded = jwt.verify(token, 'secret');
req.user = decoded;
next();
} catch (err) {
return res.status(401).json({ message: '无效的令牌' });
}
};
// 角色中间件
const roleMiddleware = (roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({ message: '权限不足' });
}
next();
};
};
// 使用中间件
app.get('/admin',
authMiddleware,
roleMiddleware(['admin']),
(req, res) => {
res.json({ message: '管理员面板' });
}
);NestJS 写法:
typescript
@Injectable()
export class AuthGuard implements CanActivate {
constructor(private jwtService: JwtService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization;
if (!token) {
throw new UnauthorizedException('未提供令牌');
}
try {
const decoded = await this.jwtService.verifyAsync(token);
request.user = decoded;
return true;
} catch {
throw new UnauthorizedException('无效的令牌');
}
}
}
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const roles = this.reflector.get<string[]>('roles', context.getHandler());
const request = context.switchToHttp().getRequest();
const user = request.user;
return roles.includes(user.role);
}
}
// 使用守卫
@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)
@Roles(['admin'])
export class AdminController {
@Get()
getAdminPanel() {
return { message: '管理员面板' };
}
}守卫的类型 🎯
1. 路由守卫
用于保护特定的路由或控制器:
typescript
@Injectable()
export class ApiKeyGuard implements CanActivate {
constructor(private configService: ConfigService) {}
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const apiKey = request.headers['x-api-key'];
const validApiKey = this.configService.get<string>('API_KEY');
return apiKey === validApiKey;
}
}
// 使用路由守卫
@Controller('api')
@UseGuards(ApiKeyGuard)
export class ApiController {
// 受保护的路由
}2. 全局守卫
应用于所有路由:
typescript
// main.ts
const app = await NestFactory.create(AppModule);
app.useGlobalGuards(new RateLimitGuard());3. 方法级守卫
只保护特定的方法:
typescript
@Controller('users')
export class UsersController {
@UseGuards(AdminGuard)
@Post()
create(@Body() createUserDto: CreateUserDto) {
// 只有管理员可以创建用户
}
}自定义守卫 🛠️
1. 基于角色的访问控制(RBAC)
typescript
// decorators/roles.decorator.ts
export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// guards/roles.guard.ts
@Injectable()
export class RolesGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredRoles = this.reflector.getAllAndOverride<string[]>('roles', [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredRoles.some((role) => user.roles?.includes(role));
}
}
// 使用示例
@Controller('reports')
export class ReportsController {
@Get()
@Roles('admin', 'manager')
@UseGuards(AuthGuard, RolesGuard)
getAllReports() {
// 只有管理员和经理可以访问
}
}2. 基于权限的访问控制(PBAC)
typescript
// decorators/permissions.decorator.ts
export const RequirePermissions = (...permissions: string[]) =>
SetMetadata('permissions', permissions);
// guards/permissions.guard.ts
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.get<string[]>(
'permissions',
context.getHandler()
);
if (!requiredPermissions) {
return true;
}
const { user } = context.switchToHttp().getRequest();
return requiredPermissions.every(
permission => user.permissions?.includes(permission)
);
}
}
// 使用示例
@Controller('documents')
export class DocumentsController {
@Post()
@RequirePermissions('create:documents')
@UseGuards(AuthGuard, PermissionsGuard)
createDocument(@Body() doc: CreateDocumentDto) {
// 只有具有创建文档权限的用户可以访问
}
}实战示例 💡
让我们创建一个完整的认证授权系统:
typescript
// guards/jwt-auth.guard.ts
@Injectable()
export class JwtAuthGuard implements CanActivate {
constructor(
private jwtService: JwtService,
private configService: ConfigService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = this.extractTokenFromHeader(request);
if (!token) {
throw new UnauthorizedException();
}
try {
const payload = await this.jwtService.verifyAsync(token, {
secret: this.configService.get('JWT_SECRET')
});
// 将用户信息添加到请求对象
request.user = payload;
return true;
} catch {
throw new UnauthorizedException();
}
}
private extractTokenFromHeader(request: Request): string | undefined {
const [type, token] = request.headers.authorization?.split(' ') ?? [];
return type === 'Bearer' ? token : undefined;
}
}
// guards/throttle.guard.ts
@Injectable()
export class ThrottleGuard implements CanActivate {
private readonly store = new Map<string, number[]>();
canActivate(context: ExecutionContext): boolean {
const request = context.switchToHttp().getRequest();
const ip = request.ip;
const now = Date.now();
const windowMs = 60000; // 1分钟
const max = 100; // 最大请求数
const requests = this.store.get(ip) || [];
const windowStart = now - windowMs;
// 清理旧的请求记录
const recentRequests = requests.filter(time => time > windowStart);
if (recentRequests.length >= max) {
throw new HttpException('Too Many Requests', 429);
}
recentRequests.push(now);
this.store.set(ip, recentRequests);
return true;
}
}
// controllers/users.controller.ts
@Controller('users')
@UseGuards(JwtAuthGuard)
export class UsersController {
constructor(private usersService: UsersService) {}
@Get('profile')
getProfile(@Request() req) {
return this.usersService.findOne(req.user.id);
}
@Patch('profile')
@UseGuards(ThrottleGuard)
updateProfile(
@Request() req,
@Body() updateUserDto: UpdateUserDto
) {
return this.usersService.update(req.user.id, updateUserDto);
}
@Delete()
@Roles(['admin'])
@UseGuards(RolesGuard)
removeUser(@Param('id') id: string) {
return this.usersService.remove(id);
}
}最佳实践 ✨
1. 组合使用守卫
typescript
// 创建组合守卫
const Auth = (...roles: string[]) => {
return applyDecorators(
UseGuards(JwtAuthGuard, RolesGuard),
Roles(roles),
);
};
// 使用组合守卫
@Controller('admin')
export class AdminController {
@Get()
@Auth('admin')
getAdminDashboard() {
return { message: '管理员仪表板' };
}
}2. 异步守卫处理
typescript
@Injectable()
export class AsyncAuthGuard implements CanActivate {
constructor(private authService: AuthService) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const token = request.headers.authorization;
try {
const user = await this.authService.validateToken(token);
request.user = user;
return true;
} catch (error) {
throw new UnauthorizedException();
}
}
}3. 错误处理
typescript
@Injectable()
export class ErrorHandlingGuard implements CanActivate {
async canActivate(context: ExecutionContext): Promise<boolean> {
try {
// 尝试验证
return true;
} catch (error) {
if (error instanceof TokenExpiredError) {
throw new UnauthorizedException('令牌已过期');
}
if (error instanceof JsonWebTokenError) {
throw new UnauthorizedException('无效的令牌');
}
throw new InternalServerErrorException('认证过程中发生错误');
}
}
}总结 📝
通过这篇文章,我们深入学习了:
- 守卫的基本概念和作用
- 不同类型的守卫及其使用场景
- 如何创建和使用自定义守卫
- 实际项目中的认证授权实现
- 守卫的最佳实践
守卫是 NestJS 中处理认证授权的最佳选择,它提供了:
- 集中的认证逻辑处理
- 灵活的授权控制
- 良好的代码组织方式
- 强大的类型支持
下一篇文章,我们将探讨 NestJS 的拦截器(Interceptor)系统。