守卫
1. 什么是守卫?这次是“贴身保镖”
守卫 (Guard) 是一个实现了 CanActivate
接口的类,它的核心职责只有一个:授权 (Authorization)。 守卫会根据运行时的一些条件(比如用户的角色、权限等)来决定一个请求是否能够被处理。
想象一下这个场景:
- 中间件 (前台):检查了你的访客证(比如 JWT 令牌),确认了你的基本身份,然后让你进公司大门。
- 守卫 (保镖):在你尝试进入 CEO 办公室时拦住你,检查你的身份牌,确认你是否有“管理员”或“高管”权限。只有权限足够,你才能进入。
守卫的核心是 canActivate()
方法。这个方法必须返回一个布尔值(或一个返回布尔值的 Promise
/ Observable
),告诉 NestJS 是否允许这个请求通过。
- 返回
true
:请求被放行,可以继续前往路由处理函数。 - 返回
false
:NestJS 会直接拒绝该请求,并通常会抛出一个403 Forbidden
的错误。
前置知识:认证 (Authentication) vs. 授权 (Authorization)
这是一个非常关键的区别,守卫主要负责后者:
- 认证 (Authentication):回答 “你是谁?” 的过程。例如,通过用户名密码登录,验证 JWT 令牌来识别用户身份。这通常可以在中间件中完成。
- 授权 (Authorization):回答 “你被允许做什么?” 的过程。例如,确认用户 A 是“管理员”,因此可以删除文章,而用户 B 是“访客”,只能阅读文章。这正是守卫的用武之地。
执行顺序:守卫在所有中间件之后执行,但在任何拦截器或管道之前执行。
2. 如何创建你的第一个守卫
一个守卫就是一个用 @Injectable()
装饰的类,并实现了 CanActivate
接口。
示例:一个简单的认证守卫 (AuthGuard
)
让我们创建一个简单的守卫,它会检查请求头里是否包含一个 authorization
字段。虽然实际的认证会复杂得多(比如验证 JWT),但这个例子能很好地说明基本结构。
src/auth/auth.guard.ts
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();
// 这里的逻辑可以非常复杂,比如验证 JWT
// 我们这里简化为检查是否存在 authorization 头
console.log('[AuthGuard] 检查请求头...');
return !!request.headers.authorization;
}
}
CanActivate
接口: 强制你实现canActivate
方法。ExecutionContext
: 这是守卫比中间件更强大的地方。 它不仅能获取到request
和response
对象,还能获取到即将要执行的控制器类和处理函数的信息,这为我们实现更复杂的逻辑(比如基于角色的访问控制)提供了可能。
3. 如何应用守卫?使用 @UseGuards()
装饰器
和 NestJS 的其他组件一样,守卫可以通过 @UseGuards()
装饰器被应用在不同的层级。
应用在单个路由上 (方法级别)
import { Controller, Post, UseGuards, Body } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
@Controller('posts')
export class PostsController {
@Post()
@UseGuards(AuthGuard) // 只保护这个创建帖子的路由
createPost(@Body() postDto: any) {
return '帖子创建成功!';
}
}
现在,如果你想请求 POST /posts
,必须在请求头中附带 authorization
字段,否则就会收到 403 错误。
应用在整个控制器上 (控制器级别)
import { Controller, Get, Post, UseGuards } from '@nestjs/common';
import { AuthGuard } from '../auth/auth.guard';
@Controller('cats')
@UseGuards(AuthGuard) // 保护这个控制器下的所有路由
export class CatsController {
@Get()
findAll() {
return '所有的小猫都在这里。';
}
@Post()
create() {
return '一只新的小猫诞生了!';
}
}
现在,访问 /cats
下的任何路由都需要通过 AuthGuard
的检查。
应用为全局守卫
如果你想保护整个应用的所有路由,可以在 main.ts
中注册一个全局守卫。 src/main.ts
```typescript import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { AuthGuard } from './auth/auth.guard';
async function bootstrap() { const app = await NestFactory.create(AppModule); // app.useGlobalGuards(new AuthGuard()); // 注册为全局守卫 await app.listen(3000); } bootstrap();
> **注意**:全局守卫通常在模块中通过 `APP_GUARD` 提供者来注册,这样可以利用依赖注入,我们将在下一个例子中看到。
## 4. 进阶用法:基于角色的访问控制 (RBAC)
这是守卫最强大、最常见的应用场景。 我们要实现这样一个逻辑:只有“管理员 (admin)”角色的用户才能创建一个新的 `Cat`。
### **第一步:创建 `@Roles()` 装饰器**
我们需要一种方式告诉守卫,某个路由需要什么角色。我们可以通过自定义装饰器来附加“元数据 (Metadata)”。
**`src/auth/roles.decorator.ts`**
```typescript
import { SetMetadata } from '@nestjs/common';
export const ROLES_KEY = 'roles';
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);```
这个 `@Roles('admin')` 装饰器所做的事情,就是给路由处理函数附加一个 `roles: ['admin']` 的元数据。
### **第二步:创建 `RolesGuard`**
这个守卫会读取路由上的元数据,并和当前用户的角色进行比较。
**`src/auth/roles.guard.ts`**
```typescript
import { Injectable, CanActivate, ExecutionContext } 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 {
// 1. 通过 Reflector 获取路由处理函数上定义的角色元数据
const requiredRoles = this.reflector.getAllAndOverride<string[]>(ROLES_KEY, [
context.getHandler(),
context.getClass(),
]);
if (!requiredRoles) {
// 如果没有设置角色要求,则默认允许访问
return true;
}
// 2. 获取请求对象,并假设用户的角色信息已附加在 request.user 上
const { user } = context.switchToHttp().getRequest();
// 3. 比较用户角色和需要的角色
// 这里简单地假设 user.roles 是一个字符串数组
return requiredRoles.some((role) => user?.roles?.includes(role));
}
}
Reflector
: NestJS 提供的一个帮助类,用于轻松地读取元数据。
第三步:整合应用
现在,我们可以在控制器中同时使用 @Roles
和 @UseGuards
。
src/cats/cats.controller.ts
import { Controller, Post, UseGuards, Body } from '@nestjs/common';
import { Roles } from '../auth/roles.decorator';
import { AuthGuard } from '../auth/auth.guard'; // 假设 AuthGuard 会验证并附加 user 对象
import { RolesGuard } from '../auth/roles.guard';
@Controller('cats')
export class CatsController {
@Post()
@Roles('admin') // 步骤 1: 设置此路由需要 'admin' 角色
@UseGuards(AuthGuard, RolesGuard) // 步骤 2: 应用守卫
create(@Body() createCatDto: any) {
return '只有 admin 才能创建小猫!';
}
}```
## 5. 总结:守卫 vs. 中间件
| 特性 | 中间件 (Middleware) | 守卫 (Guard) |
| :--- | :--- | :--- |
| **主要职责** | 通用请求处理(日志、CORS、原始数据处理) | **授权** (Authorization) |
| **执行时机** | **最早**,在所有组件之前 | 中间件之后,拦截器/管道之前 |
| **上下文信息** | 只能访问 `req`, `res`, `next`,不知道将要执行的处理器 | 可访问 `ExecutionContext`,确切知道将要执行的控制器和方法 |
| **如何工作** | 调用 `next()` 将控制权传给下一个 | 返回 `true` 或 `false` 决定是否放行 |
| **典型用例** | 请求日志、安全头设置、身份认证(识别用户) | 角色检查、权限验证、ACL |
现在你应该对守卫有了深刻的理解。它是构建安全、健壮的 NestJS 应用不可或缺的一环。记住,**守卫决定了“谁能做什么”**,这是保护你应用资源的核心。