Nestjs框架: 基于策略的权限控制(ACL)与数据权限设计

概述

  • 我们已经了解了基础的 RBAC(基于角色的访问控制)模型之后,即将进入权限系统设计的一个新进阶,也就是基于策略的权限控制(ACL)与字段级数据权限的设计与实现,如下图
  • 这一部分的核心内容聚焦在上图右侧虚线框所展示的权限控制逻辑结构中,特别是 policy(即策略模块)的实现

  • 重点内容

    • RBAC与ACL的区别与互补关系
    • 数据权限控制的颗粒度问题
    • 如何设计基于策略的数据库结构
    • 代码实现思路与策略逻辑设计

权限模型的基本结构

我们之前的权限系统设计是基于角色的访问控制(RBAC),其核心思想是在接口和 Controller 上添加装饰器,通过角色来控制用户能否访问某个接口,在数据库中配置角色与接口权限的映射关系,通过Guard守卫机制在请求时验证用户是否有权限访问该接口

这个模型控制的是接口级别的访问权限,即哪些角色可以访问哪些接口,这种模型在大多数业务场景中已经足够使用,但在某些对安全级别要求更高的系统中,仅仅控制接口级别的访问是不够的,需要更细粒度的控制,这就引出了基于策略的权限控制(ACL)

例如,在 CMS(内容管理系统)中,用户 A 是某篇文章的作者,用户 B 是普通用户。我们希望用户 A 可以编辑和更新自己的文章,而用户 B 只能阅读,不能修改。这种情况下,仅靠 RBAC 是无法实现的,因为 RBAC 干预的是接口级别的权限,而非数据级别的访问控制

我们要实现的权限控制体系,主要集中在菜单权限和接口权限两个层面:

  • 菜单权限:指的是用户在前端页面中可以访问的页面或模块。
  • 数据权限:则是更细粒度的权限控制,可以控制到数据库字段级别,例如哪些角色可以查看、修改某张表的某些字段

引入ACL权限模型的必要性

接下来我们要实现的是ACL(Access Control List)模型,也就是基于策略的权限控制。它与RBAC并不冲突,而是在RBAC的基础上进行进一步的数据权限细化

1 )RBAC无法满足的场景

如果我们仅依赖RBAC模型,例如给角色AAA分配了postUpdate权限,那么该角色下的所有用户都拥有所有post的更新权限,这显然是不安全的

这就需要我们引入基于策略的控制逻辑,例如:

ts 复制代码
// 示例:基于策略的Post更新逻辑(伪代码)
if (user.role  === 'author' && post.authorId  === user.id)  {
  allowUpdate();
} else {
  denyUpdate();
}

在 RBAC 模型中,我们通过角色来控制某个用户是否可以访问某个接口,比如:

typescript 复制代码
@Roles('admin')
@Put('post')
updatePost() {
  // ...
}

但如果我们想让某个角色只能修改自己创建的文章呢?这时候我们就需要在接口层面之外,再加入一个策略判断逻辑,例如:

typescript 复制代码
if (post.authorId === userId) {
  // 允许修改 
} else {
  // 抛出权限异常
}

但问题来了:如果系统中有多个类似的接口(例如 postcommentuserlog 等等),我们在每一个接口中都去写这样的判断逻辑,就会违反DRY 原则(Don't Repeat Yourself),也会导致代码冗余、维护困难

2 ) 为什么需要引入ACL?

我们需要控制的是:

  • 谁(用户)
  • 能访问哪些表
  • 这些表的哪些字段
  • 以及访问时的条件逻辑

当我们需要对数据库字段进行精细控制时,RBAC就显得不够用了,例如:

  • 某个用户只能查看某张表中的部分字段
  • 某个用户只能修改自己创建的记录
  • 某些字段仅对特定角色可见
  • 这些需求超出了RBAC的能力范围,就需要引入基于策略的权限控制,也就是ACL

这在某些安全要求极高的系统中非常关键。例如:

  • 在一个博客系统或CMS内容管理系统中,某个用户只能修改自己发布的文章(post),而其他用户只能阅读,不能修改。

3 ) ACL vs RBAC:二者如何互补?

  • RBAC:控制接口级别的访问权限(如用户能否访问 /post/update 接口)
  • ACL:控制数据字段级别的访问权限(如用户能否修改 post.content 字段)
  • 它们可以共存,并且互为补充,从而构建一个完整的权限体系

ACL策略的组成部分

组成部分 说明
目标对象(Subject) 用户或角色
资源对象(Resource) 表名、字段名
策略(Policy) 控制逻辑,如"创建者才能修改"
操作权限(Action) 允许的操作类型(如 read、write)

字段级别权限控制的实现难点

我们需要解决的问题:

  • 如何控制字段的访问权限?
  • 如何判断用户是否可以操作某条数据?
  • 如何将策略逻辑动态化,使其可配置、可扩展?

举个例子:

  • 假设我们有一个博客系统,其中 post 表包含如下字段:

    sql 复制代码
    CREATE TABLE post (
        id INT PRIMARY KEY,
        title VARCHAR(255),
        content TEXT,
        author_id INT,
        created_at DATETIME
    );

我们需要实现以下权限控制:

  • 普通用户只能阅读 title 和 content
  • 作者可以修改自己的 content
  • 管理员可以修改所有字段
  • 审计员只能查看 title 和 created_at
  • 如果我们仅靠RBAC模型,无法做到字段级别的控制。这就需要通过ACL策略来实现

引入 ACL(基于策略的权限控制)

为了解决上述问题,我们引入 ACL(Access Control List)权限控制机制,其核心是:

  • 定义策略(Policy)
  • 将策略与角色或用户绑定
  • 在数据访问时动态加载策略并执行判断

ACL 控制的核心优势在于:它可以在数据层(如数据库查询)进行字段、记录级别的权限控制,而不仅仅停留在接口层面。

示例场景

假设我们有一个 Post 表,其中包含字段:

sql 复制代码
id | title | content | author_id | status

我们希望:

  • 只有作者和管理员可以更新 content 字段
  • 所有用户都可以查看 titlestatus

这种情况下,我们就可以为角色定义如下策略:

角色 表名 字段名 操作权限(update/select) 逻辑表达式
author Post content update post.author_id == user.id
admin Post content update always true
all_users Post title select always true
all_users Post status select always true

这样,系统在执行数据库查询或更新操作时,会自动根据当前用户的角色和策略,动态地控制其对字段的访问权限

将策略信息动态化、持久化

为了实现策略的动态配置与管理,我们需要将 ACL 策略信息存入数据库中,并在运行时动态加载,这样才能实现真正的动态权限系统。

1 ) 设计思路

为了解决上述问题,我们需要将权限控制逻辑动态化、结构化、可配置化,并将其存储在数据库中,以便支持灵活的策略配置。

核心数据结构设计,我们需要设计以下几个关键表结构:

  1. roles(角色表)
  2. policies(策略表)
  3. role_policies(角色-策略关联表)
  4. policy_conditions(策略条件表)
  5. tables(实体表名表)
  6. fields(字段表)

关键字段说明

  • policy_type:策略类型,如read, write, delete
  • resource_type:资源类型,如post, comment, user
  • field:字段名(如title, content, status)
  • condition:条件逻辑(如created_by == user_id)

关联逻辑

  • 一个角色可以拥有多个权限
  • 一个权限可以对应多个角色
  • 一个权限可以绑定多个策略
  • 一个策略可以绑定多个字段权限
  • 一个字段权限可以绑定多个策略条件逻辑

数据库设计建议,我们可以设计如下几张表来存储策略信息:

示例表结构(简化版)

sql 复制代码
CREATE TABLE roles (
  id INT PRIMARY KEY,
  name VARCHAR(50) NOT NULL
);
 
CREATE TABLE policies (
  id INT PRIMARY KEY,
  name VARCHAR(100),
  description TEXT
);
 
CREATE TABLE role_policies (
  role_id INT,
  policy_id INT,
  FOREIGN KEY (role_id) REFERENCES roles(id),
  FOREIGN KEY (policy_id) REFERENCES policies(id)
);
 
CREATE TABLE tables (
  id INT PRIMARY KEY,
  name VARCHAR(100)
);
 
CREATE TABLE fields (
  id INT PRIMARY KEY,
  name VARCHAR(100),
  table_id INT,
  FOREIGN KEY (table_id) REFERENCES tables(id)
);
 
CREATE TABLE policy_conditions (
  policy_id INT,
  field_id INT,
  operation VARCHAR(10),
  condition TEXT,
  FOREIGN KEY (policy_id) REFERENCES policies(id),
  FOREIGN KEY (field_id) REFERENCES fields(id)
);

接口与策略的绑定逻辑

在RBAC中,我们通常将权限与接口绑定,从而实现接口访问控制。但在ACL模型中,我们的目标是:

  • 接口访问权限 + 数据字段访问权限 + 数据访问逻辑条件

这就要求我们不仅要判断用户是否有接口权限,还要判断其对访问数据的字段是否有权限,以及是否满足数据逻辑条件。

示例逻辑流程(伪代码)

ts 复制代码
function checkAccess(userId, resourceId, resourceType, action) {
  const user = getUserById(userId);
  const resource = getResource(resourceType, resourceId);
 
  // RBAC检查:用户是否有访问该接口的权限 
  if (!hasPermission(user, action, resourceType)) {
    return false;
  }
 
  // ACL检查:字段访问权限
  const allowedFields = getAllowedFields(user, resourceType);
  if (!allowedFields.includes(action.field))  {
    return false;
  }
 
  // 策略逻辑检查
  const policyRules = getPolicyRules(user, resourceType, action);
  if (!evaluatePolicyRules(policyRules, resource)) {
    return false;
  }
 
  return true;
}

在 NestJS 中使用 Guard

在 NestJS 中,Guard 是实现权限控制的核心机制之一。我们可以在 Controller 或方法上使用装饰器来声明策略需求,然后在 Guard 中统一处理权限逻辑。这样不仅提高了代码的复用性,也增强了系统的可维护性。

示例装饰器定义

typescript 复制代码
export const Policy = (metadata: PolicyMetadata) =>
  ReflectMetadata('policy', metadata);

使用方式

typescript 复制代码
@Put('post/:id')
@Policy({ table: 'Post', field: 'content', operation: 'update' })
updatePost(@Param('id') id: number, @Body() dto: UpdatePostDto) {
  // ...
}

实现思路:策略执行的逻辑流程

  1. 用户请求接口(如 /post/update)获取当前用户的角色和ID
  2. Guard 拦截请求,获取当前用户角色
  3. 根据角色从数据库中查询ACL策略表,判断该用户是否拥有相应操作权限
  4. 解析当前请求所涉及的表、字段和操作
  5. 根据策略条件执行具体逻辑判断(如是否是创建者)
  6. 匹配策略条件,判断是否允许访问
  7. 如果允许,继续执行接口逻辑;否则抛出权限异常

伪代码示例:

ts 复制代码
// 假设在 controller 中调用
async function updatePost(postId: number, content: string, user: User) {
    const post = await Post.findOne(postId); 
 
    // 检查是否允许修改 content 字段
    const isAllowed = await checkAclPolicy(
        user.role_id, 
        'post',
        'content',
        'write',
        { user_id: user.id,  author_id: post.author_id  }
    );
 
    if (!isAllowed) {
        throw new ForbiddenException('没有权限修改该字段');
    }
 
    await Post.updateOne(postId,  { content });
}

策略检查函数伪实现:

ts 复制代码
async function checkAclPolicy(roleId: number, table: string, field: string, action: string, context: any): Promise<boolean> {
    const policy = await AclPolicy.findOne({ 
        where: {
            role_id: roleId,
            table_name: table,
            field_name: field,
            allowed_actions: Like(`%${action}%`)
        }
    });
 
    if (!policy) return false;
 
    // 执行策略条件判断
    const condition = policy.policy_condition; 
    const result = evalCondition(condition, context); // 例如:author_id == user_id
    return result;
}

evalCondition 是一个策略表达式解析器,可根据实际需求采用表达式引擎(如 expr 或自定义解析)

策略可以动态从数据库加载,支持配置化管理

技术实现建议与扩展方向

策略引擎设计:建议使用策略引擎或规则引擎(如json-rules-engine)来实现策略逻辑的解耦

字段权限缓存:为提升性能,可将字段权限缓存在Redis中

策略配置界面:提供一个前端配置界面,供管理员动态配置策略

日志审计:记录每一次权限判断的过程,便于后续审计与调试

ACL 与 RBAC 的关系与区别

对比项 RBAC(基于角色) ACL(基于策略)
控制粒度 接口、路由级别 字段、记录、逻辑级别
实现方式 角色绑定权限 策略绑定角色或用户,策略定义具体逻辑
灵活性 相对固定,适合通用权限 高度灵活,适合高安全级别或精细控制的场景
适用场景 多数系统的基础权限控制 需要字段级、记录级权限控制的系统,如 CMS、ERP
代码复杂度 稍高,需要策略解析与执行引擎
是否符合 DRY 原则 否,在多个接口中重复逻辑 是,策略可复用,集中管理
相关推荐
三十_20 小时前
【NestJS】构建可复用的数据存储模块 - 动态模块
前端·后端·nestjs
SuperheroMan8246620 小时前
部署时报错:Type 'string' is not assignable to type 'never'(Prisma 关联字段问题)
nestjs
郭俊强2 天前
nestjs 缓存配置及防抖拦截器
缓存·nestjs·防抖
用户800153635506 天前
在 Nest.js 中实现文件上传
nestjs
三十_7 天前
NestJS 开发必备:HTTP 接口传参的 5 种方式总结与实战
前端·后端·nestjs
关山月10 天前
使用Nest.js设计RBAC权限系统:分步指南
nestjs
百罹鸟11 天前
nestjs 从零开始 一步一步实践
前端·node.js·nestjs
江湖人称小鱼哥12 天前
主流技术栈 NestJS、TypeScript、Node.js版本使用统计
typescript·node.js·nestjs