NestJS守卫 (Guards)

学习目标

  • 理解守卫在NestJS中的作用和地位
  • 掌握守卫的创建和基本使用方法
  • 学会实现认证守卫和授权守卫
  • 理解角色基础访问控制的实现原理
  • 能够应用守卫保护API端点

核心知识点

1. 守卫概念

守卫是NestJS中用于权限控制的组件,它们在请求处理管道中的位置如下:

  1. 客户端发送请求
  2. 中间件处理请求
  3. 守卫验证权限
  4. 管道处理数据
  5. 控制器处理请求
  6. 服务执行业务逻辑
  7. 拦截器处理响应
  8. 异常过滤器处理异常
  9. 响应返回给客户端

守卫的主要作用:

  • 验证用户身份
  • 检查用户权限
  • 保护API端点
  • 基于角色或策略进行访问控制

2. 守卫的创建

创建守卫需要实现CanActivate接口:

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 true;
  }
}

3. 守卫的应用

控制器级别

使用@UseGuards()装饰器在控制器级别应用守卫:

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

@Controller('cats')
@UseGuards(AuthGuard)
export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }
}

方法级别

使用@UseGuards()装饰器在方法级别应用守卫:

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

@Controller('cats')
export class CatsController {
  @Get()
  findAll() {
    return 'This action returns all cats';
  }

  @Get(':id')
  @UseGuards(AuthGuard)
  findOne() {
    return 'This action returns a cat';
  }
}

全局级别

在应用程序级别注册全局守卫:

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

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

4. 认证守卫实现

创建一个基于JWT的认证守卫:

// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    
    if (!token) {
      throw new UnauthorizedException('No token provided');
    }

    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: process.env.JWT_SECRET,
      });
      // 将用户信息添加到请求对象中
      request.user = payload;
    } catch {
      throw new UnauthorizedException('Invalid token');
    }

    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}

5. 角色守卫实现

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

// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } 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.get<string[]>('roles', context.getHandler());
    
    if (!requiredRoles) {
      return true; // 如果没有指定角色,则允许访问
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user;
    
    // 检查用户是否拥有所需角色
    const hasRole = requiredRoles.some((role) => user?.roles?.includes(role));
    
    if (!hasRole) {
      throw new ForbiddenException('Insufficient permissions');
    }

    return true;
  }
}

6. 角色装饰器

创建一个自定义装饰器,用于指定路由所需的角色:

// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

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

7. 组合使用

组合使用认证守卫和角色守卫:

import { Controller, Get, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';

@Controller('admin')
@UseGuards(AuthGuard, RolesGuard)
export class AdminController {
  @Get()
  @Roles('admin')
  getDashboard() {
    return 'Admin dashboard';
  }

  @Get('users')
  @Roles('admin', 'moderator')
  getUsers() {
    return 'User list';
  }
}

8. 守卫的依赖注入

守卫可以通过依赖注入系统注入其他服务:

// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { AuthService } from './auth.service';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

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

9. 守卫链

可以同时应用多个守卫,它们会按照注册顺序执行:

@Controller('protected')
@UseGuards(AuthGuard, RolesGuard, ThrottlerGuard)
export class ProtectedController {
  // 控制器方法
}

10. 上下文类型

守卫可以处理不同类型的上下文,不仅仅是HTTP请求:

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

@Injectable()
export class WsAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    // WebSocket上下文
    if (context.getType() === 'ws') {
      const client = context.switchToWs().getClient();
      // 实现WebSocket认证逻辑
      return true;
    }
    
    // HTTP上下文
    if (context.getType() === 'http') {
      const request = context.switchToHttp().getRequest();
      // 实现HTTP认证逻辑
      return true;
    }
    
    return false;
  }
}

实践案例分析

案例:基于角色的访问控制系统

需求分析

我们需要创建一个基于角色的访问控制系统,包括:

  • 用户认证(基于JWT)
  • 角色授权(管理员、普通用户)
  • 保护不同级别的API端点
  • 提供清晰的错误信息

实现步骤

  1. 创建认证服务
  2. 创建JWT模块
  3. 创建认证守卫
  4. 创建角色守卫和装饰器
  5. 创建受保护的控制器
  6. 测试访问控制

代码实现

1. 创建认证服务
// auth.service.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class AuthService {
  // 模拟用户数据
  private users = [
    { id: 1, username: 'admin', password: 'admin123', roles: ['admin'] },
    { id: 2, username: 'user', password: 'user123', roles: ['user'] },
  ];

  constructor(private jwtService: JwtService) {}

  async validateUser(username: string, password: string) {
    const user = this.users.find(u => u.username === username && u.password === password);
    if (!user) {
      return null;
    }
    
    const { password: pass, ...result } = user;
    return result;
  }

  async login(user: any) {
    const payload = { username: user.username, sub: user.id, roles: user.roles };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }

  async validateToken(token: string) {
    try {
      const payload = this.jwtService.verify(token, {
        secret: process.env.JWT_SECRET || 'secret_key',
      });
      return payload;
    } catch {
      return null;
    }
  }
}
2. 创建JWT模块
// jwt.module.ts
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';

@Module({
  imports: [
    JwtModule.register({
      secret: process.env.JWT_SECRET || 'secret_key',
      signOptions: { expiresIn: '1h' },
    }),
  ],
  exports: [JwtModule],
})
export class JwtAuthModule {}
3. 创建认证守卫
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';
import { Request } from 'express';

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private authService: AuthService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    
    if (!token) {
      throw new UnauthorizedException('No token provided');
    }

    const user = await this.authService.validateToken(token);
    
    if (!user) {
      throw new UnauthorizedException('Invalid token');
    }

    // 将用户信息添加到请求对象中
    request.user = user;
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(' ') ?? [];
    return type === 'Bearer' ? token : undefined;
  }
}
4. 创建角色守卫和装饰器
// roles.decorator.ts
import { SetMetadata } from '@nestjs/common';

export const Roles = (...roles: string[]) => SetMetadata('roles', roles);
// roles.guard.ts
import { Injectable, CanActivate, ExecutionContext, ForbiddenException } 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.get<string[]>('roles', context.getHandler());
    
    if (!requiredRoles) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user;
    
    if (!user) {
      throw new ForbiddenException('User not authenticated');
    }

    const hasRole = requiredRoles.some((role) => user.roles.includes(role));
    
    if (!hasRole) {
      throw new ForbiddenException('Insufficient permissions');
    }

    return true;
  }
}
5. 创建认证控制器
// auth.controller.ts
import { Controller, Post, Body, UnauthorizedException } from '@nestjs/common';
import { AuthService } from './auth.service';

@Controller('auth')
export class AuthController {
  constructor(private authService: AuthService) {}

  @Post('login')
  async login(@Body() loginDto: { username: string; password: string }) {
    const user = await this.authService.validateUser(loginDto.username, loginDto.password);
    
    if (!user) {
      throw new UnauthorizedException('Invalid credentials');
    }
    
    return this.authService.login(user);
  }
}
6. 创建受保护的控制器
// users.controller.ts
import { Controller, Get, Post, Body, UseGuards } from '@nestjs/common';
import { AuthGuard } from './auth.guard';
import { RolesGuard } from './roles.guard';
import { Roles } from './roles.decorator';

@Controller('users')
@UseGuards(AuthGuard, RolesGuard)
export class UsersController {
  // 所有认证用户都可以访问
  @Get('profile')
  getProfile(@Body() req: any) {
    return req.user;
  }

  // 只有管理员可以访问
  @Get('all')
  @Roles('admin')
  getAllUsers() {
    return [
      { id: 1, username: 'admin' },
      { id: 2, username: 'user' },
    ];
  }

  // 只有管理员可以创建用户
  @Post()
  @Roles('admin')
  createUser(@Body() user: { username: string; password: string }) {
    return { id: 3, ...user };
  }
}
7. 创建模块
// auth.module.ts
import { Module } from '@nestjs/common';
import { JwtAuthModule } from './jwt.module';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { AuthGuard } from './auth.guard';
import { RolesGuard } from './roles.guard';

@Module({
  imports: [JwtAuthModule],
  providers: [AuthService, AuthGuard, RolesGuard],
  controllers: [AuthController],
  exports: [AuthService, AuthGuard, RolesGuard],
})
export class AuthModule {}
// users.module.ts
import { Module } from '@nestjs/common';
import { UsersController } from './users.controller';
import { AuthModule } from './auth.module';

@Module({
  imports: [AuthModule],
  controllers: [UsersController],
})
export class UsersModule {}
// app.module.ts
import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [AuthModule, UsersModule],
})
export class AppModule {}

测试结果

1. 用户登录

请求

POST /auth/login
Content-Type: application/json

{
  "username": "admin",
  "password": "admin123"
}

响应

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}
2. 访问个人资料(所有认证用户)

请求

GET /users/profile
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

响应

{
  "username": "admin",
  "sub": 1,
  "roles": ["admin"]
}
3. 访问用户列表(仅管理员)

请求

GET /users/all
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

响应

[
  { "id": 1, "username": "admin" },
  { "id": 2, "username": "user" }
]
4. 普通用户尝试访问管理员资源

请求

POST /auth/login
Content-Type: application/json

{
  "username": "user",
  "password": "user123"
}

响应

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

请求

GET /users/all
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

响应

{
  "statusCode": 403,
  "message": "Insufficient permissions",
  "error": "Forbidden"
}
5. 未认证用户尝试访问受保护资源

请求

GET /users/profile

响应

{
  "statusCode": 401,
  "message": "No token provided",
  "error": "Unauthorized"
}

代码解析

  1. 认证服务

    • 实现了用户验证和JWT令牌生成功能
    • 使用@nestjs/jwt模块处理JWT相关操作
  2. 守卫实现

    • AuthGuard:验证JWT令牌并提取用户信息
    • RolesGuard:检查用户是否拥有所需角色
  3. 装饰器

    • @Roles()装饰器:用于指定路由所需的角色
  4. 控制器

    • AuthController:处理用户登录
    • UsersController:提供受保护的用户相关API
  5. 模块结构

    • 清晰的模块划分,便于维护和扩展
    • 正确的依赖注入配置

互动思考问题

  1. 思考:守卫和中间件的区别是什么?它们各自的使用场景是什么?

  2. 讨论:在实际应用中,如何设计一个灵活的角色权限系统?除了基于角色的访问控制,还有哪些访问控制模型?

  3. 实践:尝试创建一个基于策略的守卫,根据不同的业务规则进行访问控制。

  4. 挑战:如何实现一个基于权限的细粒度访问控制系统,允许对单个资源进行权限管理?

  5. 扩展:了解NestJS的@nestjs/passport模块,思考它如何与守卫配合使用来实现更复杂的认证策略。

小结

本集我们学习了NestJS守卫的核心概念和使用方法,包括:

  • 守卫的基本概念和作用
  • 守卫的创建和注册方法
  • 认证守卫的实现
  • 角色守卫和装饰器的创建
  • 守卫链的使用
  • 不同上下文类型的处理

通过实践案例,我们创建了一个完整的基于角色的访问控制系统,展示了如何使用守卫保护API端点并实现权限控制。守卫是NestJS实现安全访问控制的重要组件,它使得权限验证更加集中和可管理。

在下一集中,我们将学习NestJS的拦截器(Interceptors),了解如何使用拦截器处理请求和响应,实现响应格式化、日志记录等功能。

« 上一篇 NestJS管道 (Pipes) 下一篇 » NestJS拦截器 (Interceptors)