Skip to content
On this page

NestJS 守卫 (Guards)

NestJS 守卫是一个类,它实现了 CanActivate 接口。守卫的主要职责是根据某些条件(如权限、角色、ACL等)来确定给定请求是否由路由处理程序处理。守卫在中间件之后、管道和拦截器之前执行。

基础概念

守卫解决了应用程序中的授权问题。它们决定请求是否应该被路由处理程序处理,返回 true 允许请求继续,返回 false 则阻止请求继续。

守卫的执行时机:

  1. 中间件 - 最先执行
  2. 守卫 - 权限控制
  3. 拦截器 - 请求/响应转换
  4. 管道 - 数据验证和转换
  5. 控制器方法 - 处理请求

守卫接口

每个守卫必须实现 CanActivate 接口:

export interface CanActivate {
  canActivate(context: ExecutionContext): boolean | Promise<boolean>;
}
  • context - 执行上下文,提供对请求对象和其他相关信息的访问

创建基本守卫

让我们创建一个简单的身份验证守卫:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Observable } from 'rxjs';

@Injectable()
export class AuthGuard implements CanActivate {
  canActivate(
    context: ExecutionContext,
  ): boolean | Promise<boolean> | Observable<boolean> {
    const request = context.switchToHttp().getRequest();
    return validateUser(request.headers.authorization);
  }
}

function validateUser(auth: string): boolean {
  // 简单的身份验证逻辑
  return auth && auth.startsWith('Bearer ');
}

使用守卫

1. 在控制器中使用

import { Controller, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';

@Controller('cats')
@UseGuards(AuthGuard)
export class CatsController {
  @Get()
  findAll() {
    return 'This route is protected';
  }
}

2. 在方法级别使用

@Get(':id')
@UseGuards(AuthGuard)
findOne(@Param('id') id: string) {
  return this.catsService.findOne(id);
}

3. 全局注册

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { AuthGuard } from './common/guards/auth.guard';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalGuards(new AuthGuard());
  await app.listen(3000);
}
bootstrap();

角色守卫

创建一个基于角色的守卫:

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@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));
  }
}

使用 Reflector 来获取角色装饰器:

import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);

// 在控制器中使用
@Post()
@UseGuards(RolesGuard)
@Roles('admin')
createCat(@Body() createCatDto: CreateCatDto) {
  return this.catsService.create(createCatDto);
}

执行上下文

执行上下文提供了对请求对象的访问:

@Injectable()
export class PermissionGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const response = context.switchToHttp().getResponse();
    const handler = context.getHandler();
    const controller = context.getClass();
    
    // 访问请求信息
    console.log(request.user);
    console.log(handler.name); // 控制器方法名
    console.log(controller.name); // 控制器类名
    
    return true;
  }
}

异步守卫

守卫可以是异步的:

@Injectable()
export class AsyncAuthGuard implements CanActivate {
  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    
    // 异步验证逻辑
    const isValid = await this.validateToken(request.headers.authorization);
    
    if (!isValid) {
      throw new UnauthorizedException();
    }
    
    return true;
  }

  private async validateToken(token: string): Promise<boolean> {
    // 模拟异步验证
    return new Promise(resolve => {
      setTimeout(() => {
        resolve(token === 'valid-token');
      }, 100);
    });
  }
}

基于资源的守卫

创建一个基于特定资源的守卫:

@Injectable()
export class OwnershipGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    const user = request.user;
    const params = request.params;
    
    // 检查用户是否拥有该资源
    return user.id === params.userId || user.role === 'admin';
  }
}

// 在控制器中使用
@Put(':id')
@UseGuards(OwnershipGuard)
update(@Param('id') id: string, @Body() updateCatDto: UpdateCatDto) {
  return this.catsService.update(id, updateCatDto);
}

组合守卫

可以同时使用多个守卫:

@Controller('cats')
@UseGuards(AuthGuard, RolesGuard, PermissionGuard)
export class CatsController {
  @Get()
  findAll() {
    return 'Protected route with multiple guards';
  }
}

守卫与依赖注入

守卫可以使用依赖注入:

import { Injectable, CanActivate, ExecutionContext, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';

@Injectable()
export class JwtAuthGuard implements CanActivate {
  constructor(@Inject('AUTH_SERVICE') private readonly client: ClientProxy) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];

    try {
      const user = await this.client.send({ cmd: 'validate_token' }, token);
      request.user = user;
      return true;
    } catch (error) {
      return false;
    }
  }
}

守卫生命周期

守卫的执行顺序和生命周期:

@Injectable()
export class LifecycleGuard implements CanActivate {
  constructor() {
    console.log('Guard instance created');
  }

  canActivate(context: ExecutionContext): boolean {
    console.log('Guard canActivate method called');
    
    const request = context.switchToHttp().getRequest();
    
    // 守卫逻辑
    const canActivate = this.validateRequest(request);
    
    console.log('Guard validation result:', canActivate);
    return canActivate;
  }

  private validateRequest(request: any): boolean {
    // 实际验证逻辑
    return !!request.headers.authorization;
  }
}

守卫最佳实践

1. 错误处理

@Injectable()
export class ErrorHandlingGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest();
    
    try {
      if (!request.headers.authorization) {
        throw new UnauthorizedException('Authorization header required');
      }
      
      // 其他验证逻辑
      return this.validateToken(request.headers.authorization);
    } catch (error) {
      // 记录错误
      console.error('Guard validation failed:', error);
      throw error;
    }
  }

  private validateToken(token: string): boolean {
    // 验证逻辑
    return token.startsWith('Bearer ');
  }
}

2. 缓存验证结果

@Injectable()
export class CachedAuthGuard implements CanActivate {
  private cache = new Map<string, { valid: boolean; timestamp: number }>();

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization;

    const cached = this.cache.get(token);
    if (cached && Date.now() - cached.timestamp < 5 * 60 * 1000) { // 5分钟缓存
      return cached.valid;
    }

    const isValid = await this.validateToken(token);
    this.cache.set(token, { valid: isValid, timestamp: Date.now() });

    return isValid;
  }

  private async validateToken(token: string): Promise<boolean> {
    // 异步验证逻辑
    return token.startsWith('Bearer ');
  }
}

3. 守卫工厂

创建一个守卫工厂来生成参数化守卫:

export const createRoleGuard = (requiredRole: string) => {
  @Injectable()
  class RoleGuard implements CanActivate {
    canActivate(context: ExecutionContext): boolean {
      const request = context.switchToHttp().getRequest();
      return request.user?.role === requiredRole;
    }
  }
  
  return RoleGuard;
};

// 使用
@Controller('admin')
@UseGuards(createRoleGuard('admin'))
export class AdminController {
  // ...
}

与中间件、拦截器的关系

  • 中间件 - 处理通用请求逻辑,如日志、CORS
  • 守卫 - 处理权限控制和访问验证
  • 拦截器 - 处理请求/响应转换、缓存、日志记录
  • 管道 - 处理数据验证和转换

总结

NestJS 守卫是控制访问权限的强大工具。它们允许你根据特定条件决定请求是否应该被处理,从而保护你的应用程序免受未授权访问。

守卫的主要特点:

  • 实现 CanActivate 接口
  • 可以同步或异步执行
  • 可以访问执行上下文
  • 支持依赖注入
  • 可以在不同级别应用(方法、控制器、全局)
  • 可以组合使用多个守卫