RBAC(Role-Based Access Control,基于角色的访问控制)是目前 Web 系统中最通用、最成熟的权限架构。它的核心思想是:用户不直接与权限关联,而是通过“角色”作为中间层。

1. 核心设计思路

在 RBAC 模型中,我们需要处理三类实体和两组多对多关系:

  • 用户 (User):系统的使用者。
  • 角色 (Role):用户在系统中的身份(如管理员、财务、编辑)。
  • 权限 (Permission):具体的操作许可(如“发布文章”、“审核财务”)。

关系流转: 用户 -> 拥有角色 -> 角色关联权限 -> 用户获得权限


2. 数据库设计

这是 RBAC 的最小完备集(Minimum Viable Product),由 5 张表构成。

2.1 实体表

1. users(用户表) 存储基础用户信息。

CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    username VARCHAR(50) NOT NULL COMMENT '登录名',
    password CHAR(60) NOT NULL COMMENT '加密后的密码',
    status TINYINT NOT NULL DEFAULT 1 COMMENT '1=正常, 0=禁用'
) COMMENT='用户基础表';

安全提示:密码严禁明文存储。推荐使用 bcrypt 算法(PHP password_hashcrypt)。

2. roles(角色表) 定义系统中的身份。

CREATE TABLE roles (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(50) NOT NULL COMMENT '唯一标识, e.g. admin',
    display_name VARCHAR(100) NOT NULL COMMENT '显示名称, e.g. 管理员',
    description VARCHAR(255) DEFAULT NULL
) COMMENT='角色定义表';

3. permissions(权限点表) 定义系统中所有可控的“资源+动作”。

CREATE TABLE permissions (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL COMMENT '唯一标识, e.g. post.create',
    display_name VARCHAR(150) NOT NULL COMMENT '显示名称',
    description VARCHAR(255) DEFAULT NULL
) COMMENT='具体权限点定义表';

最佳实践:权限 Key 推荐使用 资源.动作 格式,例如:

  • user.view, user.create, user.delete
  • finance.audit, finance.export

2.2 关联表(中间表)

4. user_role(用户-角色关联) 实现用户与角色的多对多关系(一个用户可以是多个角色)。

CREATE TABLE user_role (
    user_id INT UNSIGNED NOT NULL,
    role_id INT UNSIGNED NOT NULL,
    PRIMARY KEY (user_id, role_id)
) COMMENT='用户角色关联表';

5. role_permission(角色-权限关联) 实现角色与权限的多对多关系(一个角色拥有多个权限)。

CREATE TABLE role_permission (
    role_id INT UNSIGNED NOT NULL,
    permission_id INT UNSIGNED NOT NULL,
    PRIMARY KEY (role_id, permission_id)
) COMMENT='角色权限关联表';

3. 后端核心代码实现 (PHP)

为了保证系统的可维护性,我们将权限逻辑封装在独立的层中。

目录结构建议:

/include/
    ├── auth_core.php        # 核心逻辑:登录验证、权限加载、检查函数
    ├── bootstrap_auth.php   # 引导文件:统一初始化 Session 和 Auth
/finance/
    ├── report.php           # 业务页面

3.1 核心逻辑 (auth_core.php)

<?php
// include/auth_core.php
require_once __DIR__ . '/../config/database.php';

/**
 * 启动安全 Session
 */
function auth_start_session()
{
    if (session_id() == '') {
        session_name(SESSION_NAME);
        session_start();
    }
}

/**
 * 用户登录处理
 */
function auth_login($username, $password)
{
    $pdo = db_connect();
    // 务必检查 status = 1 (激活状态)
    $stmt = $pdo->prepare("SELECT * FROM users WHERE username = ? AND status = 1 LIMIT 1");
    $stmt->execute(array($username));
    $user = $stmt->fetch();

    if (!$user) {
        return false;
    }

    // 密码校验 (兼容 PHP 5.3+ crypt 方式,PHP 5.5+ 建议用 password_verify)
    if (crypt($password, $user['password']) !== $user['password']) {
        return false;
    }

    // --- 登录成功核心逻辑 ---
    
    // 1. 一次性加载该用户所有权限和角色
    $permissions = auth_load_permissions_for_user($user['id']);
    $roles       = auth_load_roles_for_user($user['id']);

    // 2. 将权限写入 Session (避免每次刷新页面都查库)
    $_SESSION['user'] = array(
        'id'          => $user['id'],
        'username'    => $user['username'],
        'roles'       => $roles,       // 数组: ['admin', 'editor']
        'permissions' => $permissions, // 数组: ['post.create', 'post.view']
    );

    // 3. 安全防护:重置 Session ID,防止会话固定攻击
    session_regenerate_id(true);

    return true;
}

/**
 * 注销
 */
function auth_logout()
{
    auth_start_session();
    $_SESSION = array();
    if (ini_get("session.use_cookies")) {
        $params = session_get_cookie_params();
        setcookie(session_name(), '', time() - 42000,
            $params["path"], $params["domain"],
            $params["secure"], $params["httponly"]
        );
    }
    session_destroy();
}

/**
 * 获取当前登录用户信息
 */
function auth_user()
{
    auth_start_session();
    return isset($_SESSION['user']) ? $_SESSION['user'] : null;
}

/**
 * 检查是否已登录
 */
function auth_check()
{
    return auth_user() !== null;
}

// ========== 数据库查询辅助函数 ==========

function auth_load_roles_for_user($userId)
{
    $pdo = db_connect();
    $sql = "SELECT r.name FROM roles r
            JOIN user_role ur ON ur.role_id = r.id
            WHERE ur.user_id = ?";
    $stmt = $pdo->prepare($sql);
    $stmt->execute(array($userId));
    return $stmt->fetchAll(PDO::FETCH_COLUMN); // 直接返回一维数组
}

function auth_load_permissions_for_user($userId)
{
    $pdo = db_connect();
    // 使用 DISTINCT 去重(因为用户可能有多个角色,角色间可能有重叠权限)
    $sql = "SELECT DISTINCT p.name
            FROM permissions p
            JOIN role_permission rp ON rp.permission_id = p.id
            JOIN user_role ur ON ur.role_id = rp.role_id
            WHERE ur.user_id = ?";
    $stmt = $pdo->prepare($sql);
    $stmt->execute(array($userId));
    return $stmt->fetchAll(PDO::FETCH_COLUMN);
}

// ========== 权限检查核心 API ==========

/**
 * 检查当前用户是否有特定权限
 * @param string $permission 权限Key (e.g. 'post.create')
 * @return bool
 */
function auth_can($permission)
{
    $user = auth_user();
    if (!$user) {
        return false;
    }

    // 【超级管理员后门】:如果是 super_admin,拥有所有权限
    if (in_array('super_admin', $user['roles'], true)) {
        return true;
    }

    // 严格匹配权限
    return in_array($permission, $user['permissions'], true);
}

/**
 * 强制检查权限,无权则终止执行
 */
function auth_require_permission($permission)
{
    // 1. 先检查登录
    if (!auth_check()) {
        header('Location: /login.php');
        exit;
    }
    
    // 2. 再检查权限
    if (!auth_can($permission)) {
        header('HTTP/1.1 403 Forbidden');
        echo 'Access Denied';
        exit;
    }
}

3.2 统一引导文件 (bootstrap_auth.php)

<?php
// include/bootstrap_auth.php
// 所有业务页面头部必须引用此文件

require_once __DIR__ . '/auth_core.php';

// 初始化 Session
auth_start_session();

// 可以在这里做全局的 Session 过期检查或单一登录检查

3.3 业务页面接入示例

<?php
// finance/report.php
require_once __DIR__ . '/../include/bootstrap_auth.php';

// 1. 这一行代码就完成了“未登录跳转”和“无权拦截”
auth_require_permission('finance.view_report');

// --- 下面是业务代码,只有拥有权限的人才能看到 ---
?>
<h1>财务报表</h1>
<?php if (auth_can('finance.export')): ?>
    <button>导出 Excel</button>
<?php endif; ?>

4. 最佳实践总结

  1. 零信任原则 (Default Deny)
    • auth_can() 默认返回 false
    • 没有明确赋予权限的操作,一律视为禁止。
  2. 超级管理员 (Super Admin)
    • 在代码层面为 super_admin 角色留“后门”(return true),防止因为数据库配置错误导致所有人都无法登录后台修复数据。
  3. 权限缓存
    • 本文示例将权限放入了 $_SESSION。优点是性能高(不用每次刷新查库),缺点是修改用户权限后,用户需重新登录才能生效。
    • 进阶方案:在 Session 中记录 permission_version,每次请求对比数据库版本号,若过期则重载。
  4. 前端隐藏 vs 后端拦截
    • 前端 if (auth_can(...)) 隐藏按钮只是为了用户体验,绝不能作为安全措施。
    • 所有数据接口(API)和页面入口必须有后端的 auth_require_permission() 拦截。

5. 权限模型对比

当 RBAC 无法满足需求(如需要控制“只能上午访问”、“只能访问自己创建的数据”)时,可以考虑以下模型:

模型 全称 核心逻辑 适用场景 复杂度
ACL Access Control List 用户 <-> 资源 个人网盘、极简单的后台
RBAC Role-Based Access Control 用户 <-> 角色 <-> 权限 绝大多数企业后台、SaaS 系统 ⭐⭐
ABAC Attribute-Based Access Control 属性(如时间、地点、部门)动态计算 公有云 IAM、高安全级银行系统 ⭐⭐⭐⭐
MAC Mandatory Access Control 强制标签(绝密/机密) 军工、涉密系统 ⭐⭐⭐
ReBAC Relationship-Based Access Control 基于关系图谱(我是 Owner 就能改) 社交网络、Google Drive 文档协作 ⭐⭐⭐⭐⭐