Skip to content

当然!我们来攻克安全领域的第二道大门,也是更精细的一道门:授权 (Authorization)。如果你已经理解了认证(Authentication),那么授权就是建立在认证基础之上的“权限管理中心”。

为你的应用划分“权限区域”:NestJS 授权 (Authorization) 完全指南

再次回到我们那个高度机密的科研基地。

通过认证 (Authentication),我们已经确认了门口这位访客的身份——他就是“张三研究员”(request.user 对象里现在有了他的信息)。

现在,授权 (Authorization) 要回答一个更复杂的问题:“好的,既然你是张三研究员,那么你具体能做什么?

  • 他能进入普通的 B 级实验室吗?(权限检查)
  • 他有权删除重要的实验数据吗?(操作权限检查)
  • 他是否有权查看S 级核心区的机密文件?(角色或资源权限检查)

这位“张三研究员”,就像我们应用中一个已登录的用户。授权,就是根据这个用户的身份、角色或属性,来决定他是否有权访问某个特定的资源或执行某个特定的操作。

在 NestJS 中,实现授权的核心武器就是守卫 (Guards)。我们之前在认证中已经用 AuthGuard 部署了一位“门禁保安”,现在我们要为基地的每个重要房间门口,都配备一位更智能的“区域保安”,他们手里拿着一本详细的“权限清单”。

1. 基础授权:基于角色的访问控制 (RBAC)

这是最经典、最常见的授权模型:Role-Based Access Control (RBAC)

思路

  1. 为每个用户赋予一个或多个角色 (Role)(如 admin, member, guest)。通常这个角色信息会在用户登录认证后,被包含在 request.user 对象中。
  2. 为每个需要保护的路由,声明它需要什么角色才能访问。
  3. 创建一个 RolesGuard(区域保安),他的工作就是比较用户拥有的角色和路由要求的角色,然后决定是否放行。

第一步:创建“权限标签” (@Roles 装饰器)

我们需要一种方式来给路由“贴标签”。这就需要用到我们之前学过的自定义装饰器元数据 (Metadata)

src/auth/roles.decorator.ts

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

export const ROLES_KEY = 'roles';
// Roles 是一个函数,它返回一个由 SetMetadata 创建的装饰器
// (...roles: string[]) 允许我们传递一个或多个角色字符串
export const Roles = (...roles: string[]) => SetMetadata(ROLES_KEY, roles);

现在,我们有了一个 @Roles('admin') 装饰器,可以用来给任何路由处理函数附加 roles: ['admin'] 这样的元数据。

第二步:实现“区域保安” (RolesGuard)

这位保安需要一把能读取“权限标签”的“扫描枪”——Reflector

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 {
  // 注入 Reflector
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    // 1. 使用 Reflector 读取附加在路由上的元数据 (需要的角色)
    // getAllAndOverride 会同时查找方法和类上的元数据,并以方法上的为准
    const requiredRoles = this.reflector.getAllAndOverride<string[]>(
      ROLES_KEY,
      [
        context.getHandler(), // 当前的方法
        context.getClass(), // 当前的控制器类
      ]
    );

    // 2. 如果这个路由没有 @Roles() 标签,说明它不需要特定角色,直接放行
    if (!requiredRoles) {
      return true;
    }

    // 3. 获取当前登录的用户信息 (由之前的 AuthGuard 附加)
    const { user } = context.switchToHttp().getRequest();

    // 4. 核心逻辑:判断用户的角色数组中,是否至少有一个角色是路由所需要的
    return requiredRoles.some((role) => user.roles?.includes(role));
  }
}

第三步:在控制器中部署保安和张贴标签

现在,我们可以将 AuthGuard(门禁保安)和 RolesGuard(区域保安)一起部署。

src/cats/cats.controller.ts

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

@Controller('cats')
// 在整个控制器上部署保安,确保所有接口都至少需要登录
@UseGuards(AuthGuard('jwt'), RolesGuard)
export class CatsController {
  @Get()
  findAll() {
    // 这个接口没有 @Roles 标签,所以任何登录的用户都可以访问
    return 'This action returns all cats for any logged-in user.';
  }

  @Post()
  @Roles('admin') // 贴上“仅限管理员”的标签
  create() {
    // 只有角色为 'admin' 的用户才能访问这个接口
    return 'This action adds a new cat, only for admins.';
  }
}

工作流程:

  1. 一个请求到达 POST /cats
  2. AuthGuard 先启动,验证 JWT,确认用户身份并将 user 对象(假设 user = { id: 1, roles: ['member'] })附加到 req 上。
  3. RolesGuard 接着启动。
  4. 它通过 Reflector 发现 create() 方法需要 ['admin'] 角色。
  5. 它检查 req.user.roles['member'])中是否包含 'admin'
  6. 检查结果为 falseRolesGuard 返回 false,NestJS 自动抛出一个 403 Forbidden 错误。

2. 进阶授权:声明式的声明周期 (Claims-Based Authorization)

RBAC 很简单,但有时不够灵活。比如:

  • “只有创建了这只猫的用户,才能编辑它。”
  • “只有年龄大于 18 岁的用户,才能访问这个内容。”

这些规则与“角色”无关,而与用户的具体声明/属性 (Claim) 或与资源本身的关系有关。

我们可以扩展 RolesGuard 来实现更复杂的逻辑。

src/auth/policies.guard.ts (一个更通用的守卫)

typescript
// policies.handler.ts - 定义策略处理器
export interface PolicyHandler {
  handle(ability: CaslAbility, ...args: any[]): boolean;
}

// policies.guard.ts
@Injectable()
export class PoliciesGuard implements CanActivate {
  constructor(
    private reflector: Reflector,
    private caslAbilityFactory: CaslAbilityFactory // 假设我们有一个 CASL 的能力工厂
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const policyHandlers = this.reflector.get<PolicyHandler[]>(/* ... */) || [];
    const { user } = context.switchToHttp().getRequest();

    // 使用 CASL 来创建一个“能力”对象
    const ability = this.caslAbilityFactory.createForUser(user);

    return policyHandlers.every((handler) => handler.handle(ability));
  }
}

这个例子引入了 CASL (Casl is an authorization library) 这个强大的库,它允许你定义非常精细的权限规则,比如 can('update', 'Article', { authorId: user.id })(可以更新文章,当文章的作者 ID 是当前用户 ID 时)。

3. 中国企业级的复杂权限系统方案

在大型的、多租户的 SaaS 应用或复杂的后台管理系统(如飞书、钉钉的管理后台)中,权限系统远比简单的 RBAC 要复杂。

挑战:

  1. 数据权限:同一个角色的用户(如“销售经理”),华东区的经理只能看到华东区的销售数据,华北区的经理只能看到华北区的。
  2. 动态权限:权限不是静态赋予的,而是通过复杂的规则和组织架构计算得出的。
  3. 权限组合与继承:一个用户可能同时属于多个用户组,拥有多个角色,其最终权限是这些身份权限的并集。
  4. 权限可视化管理:需要一个 UI 界面,让非技术人员(如系统管理员)可以方便地配置用户的角色和权限。

企业级方案 Demo:ABAC (Attribute-Based Access Control) + 权限中台

这是一个更现代、更灵活的模型。

  • 核心思想: 授权决策不仅仅基于用户的角色 (Role),而是基于一系列属性 (Attributes) 的组合。这些属性可以来自:
    • 用户属性 (Subject): 年龄、部门、职位、地理位置...
    • 资源属性 (Object): 文件的密级、订单的金额、客户的归属地...
    • 操作属性 (Action): 读取、写入、删除、审批...
    • 环境属性 (Environment): 当前时间、访问 IP 地址、设备类型...
  • 权限中台:
    1. 统一的权限模型定义:企业会建立一个独立的“权限中心”服务。在这个服务中,管理员可以通过界面来定义资源 (Resource)操作 (Action),并创建策略 (Policy)
    2. 策略定义: 一个典型的策略可能看起来像这样:
      • 允许 (Effect: Allow)
      • 主体 (Principal):所有职位为“部门经理”的用户
      • 操作 (Action)read, update
      • 资源 (Resource):所有属于“本部门”的“报销单”
      • 条件 (Condition):当报销单金额小于 5000 元时
    3. 决策点 (PDP - Policy Decision Point):权限中心提供一个 API,比如 can(principal, action, resource, environment)
  • 与 NestJS 的集成:
    1. 你的 NestJS 应用中会有一个非常通用AuthorizationGuard
    2. 这个守卫不再自己实现复杂的 if-else 逻辑。它的唯一职责是:
      • 从请求中收集用户、资源、操作、环境这四个维度的属性。
      • 调用权限中台的 can() API,将这些属性发送过去。
      • 根据权限中台返回的 truefalse,来决定是否放行。

AuthorizationGuard 伪代码:

typescript
@Injectable()
export class CentralAuthGuard implements CanActivate {
  constructor(private readonly permissionCenterClient: PermissionCenterClient) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const { user } = request;

    // 从请求和元数据中提取所需属性
    const action = this.reflector.get('action', context.getHandler());
    const resourceType = this.reflector.get('resource', context.getClass());
    const resourceId = request.params.id;

    // 调用权限中台进行决策
    const isAllowed = await this.permissionCenterClient.can({
      subject: { id: user.id, department: user.department, title: user.title },
      action: action,
      object: { type: resourceType, id: resourceId },
      environment: { ip: request.ip }
    });

    return isAllowed;
  }
}```
这种模式将**业务逻辑**(在 NestJS 应用中)和**权限决策逻辑**(在权限中台中)完全分离,使得权限系统可以独立演进、统一管理,极大地提高了大型系统的可维护性和安全性。这是构建复杂企业级后台的“黄金标准”。

### 总结

授权是认证的下一步,它决定了“你能做什么”。

*   **基础方案 (RBAC)**: 基于**角色**的访问控制。通过**自定义装饰器 (`@Roles`)** 和**守卫 (`RolesGuard`)** 配合 **`Reflector`** 来实现。简单、直观、能满足大部分应用的需求。
*   **进阶方案**: 使用 **CASL** 等库实现更精细的、基于**声明 (Claims)** 的授权。
*   **企业级方案 (ABAC + 权限中台)**: 将权限决策逻辑外包给一个**独立的权限中心**服务。NestJS 的守卫只负责收集属性并发起决策请求。这是构建大型、复杂、可动态配置权限系统的最佳实践。

从简单的 RBAC 开始,到理解 ABAC 的思想,掌握授权技术,你才能构建出真正安全、可靠、能应对复杂业务规则的应用程序。