人人都会AI编程

RBAC 权限模型设计与实现

更新时间:2026-07-11

认证(Authentication)确认了“你是谁”,而授权(Authorization)决定了“你能做什么”。在大多数后台管理系统中,权限控制需要做到足够灵活,既能管理不同角色的访问范围,又能对单个用户做细粒度调整。RBAC(Role-Based Access Control,基于角色的访问控制)是目前最成熟、应用最广泛的授权模型,本节我们将详细拆解它的设计思路并给出可落地的 Node.js 实现方案。

RBAC 的核心概念

RBAC 将权限分为三个核心实体:

  • 用户(User):系统的使用者,可以是管理员、编辑、普通员工等。
  • 角色(Role):一组权限的集合,例如“超级管理员”“内容编辑”“只读用户”。一个用户可以拥有一个或多个角色。
  • 权限(Permission):对某个资源(Resource)执行某个操作(Action)的许可。例如“编辑文章”“删除评论”“查看订单列表”。权限通常以 资源:操作 的格式表示(如 article:updateorder:read)。

它们的关系为:用户通过角色间接获得权限。这样设计的好处是,当需要调整某个岗位的权限时,只需修改角色所绑定的权限,而不必逐个修改用户。遇到特殊情况,也可以直接为用户授予或撤销某个特定权限,实现“用户-权限”的细粒度覆盖。

数据库表结构设计

典型的 RBAC 需要 5 张基础表(以 MySQL 为例):

-- 用户表
CREATE TABLE users (
  id INT PRIMARY KEY AUTO_INCREMENT,
  username VARCHAR(50) NOT NULL UNIQUE,
  password_hash VARCHAR(255) NOT NULL,
  status TINYINT DEFAULT 1,
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 角色表
CREATE TABLE roles (
  id INT PRIMARY KEY AUTO_INCREMENT,
  name VARCHAR(50) NOT NULL UNIQUE,       -- 角色标识,如 admin、editor
  display_name VARCHAR(100),              -- 显示名称,如“内容编辑”
  description VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 权限表
CREATE TABLE permissions (
  id INT PRIMARY KEY AUTO_INCREMENT,
  code VARCHAR(100) NOT NULL UNIQUE,      -- 权限标识,如 article:create
  display_name VARCHAR(100),              -- 显示名称,如“创建文章”
  resource VARCHAR(50),                   -- 资源
  action VARCHAR(50),                     -- 操作
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 用户-角色关联表(多对多)
CREATE TABLE user_roles (
  user_id INT NOT NULL,
  role_id INT NOT NULL,
  PRIMARY KEY (user_id, role_id),
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE
);

-- 角色-权限关联表(多对多)
CREATE TABLE role_permissions (
  role_id INT NOT NULL,
  permission_id INT NOT NULL,
  PRIMARY KEY (role_id, permission_id),
  FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE,
  FOREIGN KEY (permission_id) REFERENCES permissions(id) ON DELETE CASCADE
);

如果还需要支持“用户-权限”的直接授予,可以再增加一张 user_permissions 表,结构与 role_permissions 类似。

这套设计遵循标准的多对多关联,查询用户的最终权限时,只需将用户角色的权限合并去重即可。

权限校验的两种模式

在实际编码中,权限校验通常有两种模式:路由守卫菜单/按钮级控制

路由守卫

在 API 层通过中间件拦截请求,判断当前用户是否有访问该接口的权限。例如,一个更新文章的接口可能要求 article:update 权限。

// 权限中间件
function requirePermission(...permissions) {
  return async (req, res, next) => {
    // req.user 应由认证中间件注入,包含用户信息和权限列表
    const userPermissions = req.user.permissions; // 假设是['article:read','article:update']
    const hasPermission = permissions.some(p => userPermissions.includes(p));
    if (!hasPermission) {
      return res.status(403).json({ message: '无权限访问' });
    }
    next();
  };
}

// 使用
router.put('/articles/:id', requirePermission('article:update'), updateArticle);

如果接口权限要求较复杂(如必须同时拥有多个权限),可以将 some 改为 every

菜单与按钮控制

前端需要根据用户权限动态显示菜单和操作按钮。这通常通过后端在登录成功后返回用户的权限列表(以及角色信息)实现。前端在渲染时根据权限代码判断是否展示:

// 前端简易示例
<button v-if="permissions.includes('article:delete')">删除</button>

这样做可以提升用户体验,但必须明确:前端控制仅仅是 UI 层面的优化,真正的安全防线永远是后端权限校验。攻击者可以绕过前端直接请求 API,因此后端守卫必不可少。

权限数据的缓存与性能优化

用户的权限列表在每次请求时都可能被查询,若每次都要连接数据库并进行多表 join,会成为性能瓶颈。最常见的优化手段是在认证阶段将权限数据载入 JWT 或 Redis

方案一:放入 JWT 载荷

在用户登录时,查询其所有权限,将其编码进 JWT 的 payload 中。后续请求中,权限中间件直接从解码后的 JWT 中读取权限,无需查库。

优点:无状态,无需额外存储。
缺点:JWT 体积增大;权限变更后需要用户重新登录才能生效,不适合对实时性要求高的系统。

方案二:存储在 Redis

登录后将用户权限列表以用户 ID 为 key 缓存到 Redis,并设置合理的过期时间(如 1 小时)。权限中间件先从 Redis 获取,未命中则查询数据库并回写缓存。权限变更时主动删除对应 Redis 键。

// 登录时缓存权限
await redis.setex(`user:${userId}:permissions`, 3600, JSON.stringify(permissions));

// 中间件获取
let perms = await redis.get(`user:${userId}:permissions`);
if (!perms) {
  perms = await fetchPermissionsFromDB(userId);
  await redis.setex(`user:${userId}:permissions`, 3600, JSON.stringify(perms));
}

这种方式兼顾了性能与实时性,推荐在多数项目中使用。

权限的动态管理

权限码通常是开发阶段预定义的(如 article:create),但角色和角色的权限分配应由管理员通过后台页面配置。需要提供以下功能:

  • 角色管理:创建、编辑、删除角色。
  • 权限列表:展示系统中所有可用的权限码(可通过扫描接口或手动维护)。
  • 角色赋权:为角色勾选/取消权限。
  • 用户分配角色:为用户分配一个或多个角色。

实现上,这些都只是对 rolespermissionsrole_permissionsuser_roles 表的 CRUD 操作。需要注意的是,删除角色或权限时应当考虑外键约束,避免留下悬空关联。

一个完整的权限校验流程示例

假设使用 Koa + JWT + Redis 的典型实现,请求流程如下:

  1. 认证中间件:解析 Authorization 头中的 Bearer token,验证签名,取出用户 ID,从 Redis 或 JWT 中获取用户权限列表,挂载到 ctx.state.user
  2. 权限中间件:如 requirePermission('article:update'),检查 ctx.state.user.permissions 是否包含所需权限,不包含则直接返回 403。
  3. 业务逻辑:通过认证和授权的请求进入控制器,执行具体业务。
// 认证中间件(JWT版本)
async function authMiddleware(ctx, next) {
  const token = ctx.headers.authorization?.split(' ')[1];
  if (!token) return ctx.throw(401, '未登录');
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    // 若使用Redis缓存权限
    let permissions = await redis.get(`user:${decoded.id}:permissions`);
    if (!permissions) {
      const user = await User.findByPk(decoded.id, { include: [...] });
      permissions = extractPermissions(user);
      await redis.setex(`user:${decoded.id}:permissions`, 3600, JSON.stringify(permissions));
    } else {
      permissions = JSON.parse(permissions);
    }
    ctx.state.user = { id: decoded.id, username: decoded.username, permissions };
  } catch (err) {
    return ctx.throw(401, 'token无效');
  }
  await next();
}

这样的设计使得权限校验逻辑高度集中,业务开发者只需关心接口应该被什么权限保护,而无需重复编写权限判断代码。

落地的注意事项与扩展

  • 默认拒绝原则:没有明确允许的操作一律禁止。权限中间件应设置为“白名单”模式。
  • 超级管理员:可以硬编码一个角色(如 super_admin),该角色拥有一切权限,不参与常规权限校验。
  • 数据级权限:RBAC 本身不解决“用户只能看到自己部门的数据”这类行级权限,通常需要结合业务逻辑,在查询时动态追加过滤条件(如 where department_id = ?)。
  • 权限码的维护:随着功能增多,权限码会膨胀。建议将权限码统一定义在一个常量文件中,并配合代码注释,便于后端和前端共享。
  • 测试:为权限中间件编写单元测试,确保每种角色和权限组合的行为符合预期。

RBAC 模型虽然简单,但它为绝大部分应用提供了清晰、可扩展的授权框架。将这套模型与 JWT、Redis、现有框架中间件深度结合后,你将获得一个安全且高效的权限控制体系,足以应对从个人项目到企业级后台的授权需求。