Skip to content

什么是守卫?🤔 ​

守卫(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('认证过程中发生错误');
    }
  }
}

总结 📝 ​

通过这篇文章,我们深入学习了:

  1. 守卫的基本概念和作用
  2. 不同类型的守卫及其使用场景
  3. 如何创建和使用自定义守卫
  4. 实际项目中的认证授权实现
  5. 守卫的最佳实践

守卫是 NestJS 中处理认证授权的最佳选择,它提供了:

  • 集中的认证逻辑处理
  • 灵活的授权控制
  • 良好的代码组织方式
  • 强大的类型支持

下一篇文章,我们将探讨 NestJS 的拦截器(Interceptor)系统。

相关资源 📚 ​