怎样在PHP项目中实现角色权限?

wen java案例 2

本文目录导读:

怎样在PHP项目中实现角色权限?

  1. 核心概念
  2. 数据库设计(MySQL)
  3. 核心 PHP 实现
  4. 性能优化要点
  5. 权限命名规范(最佳实践)
  6. 完整示例:种子数据(初始化)
  7. 常见问题及解决方案

在PHP项目中实现角色权限(RBAC,基于角色的访问控制)是一个核心的安全设计问题,下面我会从数据库设计核心逻辑代码实现,提供一个完整且可落地的方案。


核心概念

在开始编码前,需要明确三个关键概念:

  1. 用户(User):系统的使用者。
  2. 角色(Role):权限的集合,管理员”、“编辑”、“普通用户”。
  3. 权限(Permission):具体的操作或资源,创建文章”、“删除用户”、“查看财务报表”。

关系:用户 → 角色 → 权限(多对多关系)


数据库设计(MySQL)

这是最标准、最灵活的 RBAC 表结构(5 张核心表):

-- 1. 用户表
CREATE TABLE `users` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `username` VARCHAR(50) NOT NULL UNIQUE,
    `password` VARCHAR(255) NOT NULL,
    `email` VARCHAR(100),
    `status` TINYINT DEFAULT 1, -- 1正常 0禁用
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 2. 角色表
CREATE TABLE `roles` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(50) NOT NULL UNIQUE,       -- 如 admin, editor
    `description` VARCHAR(200),
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 3. 权限表
CREATE TABLE `permissions` (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `name` VARCHAR(100) NOT NULL UNIQUE,      -- 如 article.create
    `label` VARCHAR(50),                      -- 中文描述(可选)
    `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 4. 用户-角色关联表(多对多)
CREATE TABLE `user_roles` (
    `user_id` INT UNSIGNED NOT NULL,
    `role_id` INT UNSIGNED 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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 5. 角色-权限关联表(多对多)
CREATE TABLE `role_permissions` (
    `role_id` INT UNSIGNED NOT NULL,
    `permission_id` INT UNSIGNED 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
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

核心 PHP 实现

权限检查类(Rbac.php

这是整个系统的核心,负责验证用户是否拥有某个权限。

<?php
class Rbac
{
    private $db; // PDO 实例
    public function __construct(PDO $db)
    {
        $this->db = $db;
    }
    /**
     * 检查用户是否拥有某个权限(支持逗号分隔的多个权限)
     * @param int    $userId
     * @param string $permissionName  'article.create'
     * @return bool
     */
    public function check($userId, $permissionName)
    {
        // 如果用户为空或权限名为空,直接返回 false
        if (empty($userId) || empty($permissionName)) {
            return false;
        }
        // 超级管理员:ID=1 或 role_id=1 拥有所有权限
        if ($this->isSuperAdmin($userId)) {
            return true;
        }
        // 查询用户的所有角色及其权限(单条 SQL 优化)
        $sql = "
            SELECT rp.permission_id
            FROM user_roles ur
            JOIN role_permissions rp ON ur.role_id = rp.role_id
            JOIN permissions p ON rp.permission_id = p.id
            WHERE ur.user_id = :user_id AND p.name = :perm_name
            LIMIT 1
        ";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([
            ':user_id'   => $userId,
            ':perm_name' => $permissionName,
        ]);
        return (bool) $stmt->fetchColumn();
    }
    /**
     * 检查是否超级管理员(可根据业务调整)
     */
    private function isSuperAdmin($userId)
    {
        // 方式一:通过角色名判断
        $sql = "SELECT 1 FROM user_roles ur 
                JOIN roles r ON ur.role_id = r.id
                WHERE ur.user_id = ? AND r.name = 'super_admin' 
                LIMIT 1";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([$userId]);
        return (bool) $stmt->fetchColumn();
        // 方式二:固定用户ID(不推荐用于生产)
        // return $userId == 1;
    }
    /**
     * 获取用户所有权限(用于前端菜单或权限列表)
     */
    public function getUserPermissions($userId)
    {
        $sql = "
            SELECT DISTINCT p.name
            FROM user_roles ur
            JOIN role_permissions rp ON ur.role_id = rp.role_id
            JOIN permissions p ON rp.permission_id = p.id
            WHERE ur.user_id = :user_id
        ";
        $stmt = $this->db->prepare($sql);
        $stmt->execute([':user_id' => $userId]);
        return $stmt->fetchAll(PDO::FETCH_COLUMN);
    }
}

在控制器中使用(中间件风格)

假设你有一个简单的 MVC 架构,在控制器方法中调用权限检查:

<?php
class ArticleController
{
    private $rbac;
    private $currentUser;
    public function __construct(Rbac $rbac, $currentUser)
    {
        $this->rbac = $rbac;
        $this->currentUser = $currentUser; // 从 session 获取的 user id
    }
    public function create()
    {
        // 权限检查
        if (!$this->rbac->check($this->currentUser['id'], 'article.create')) {
            http_response_code(403);
            echo json_encode(['error' => '无权限创建文章']);
            exit;
        }
        // 执行业务逻辑...
        echo "创建文章成功";
    }
    public function delete($id)
    {
        if (!$this->rbac->check($this->currentUser['id'], 'article.delete')) {
            http_response_code(403);
            exit('禁止删除');
        }
        // ...
    }
}

全局中间件(推荐用于框架)

如果你使用 Laravel / ThinkPHP 等框架,可以写一个中间件:

// 伪代码示例(中间件类)
class RbacMiddleware
{
    public function handle($request, $next, $permission)
    {
        $userId = session('user_id');
        $rbac   = new Rbac(DB::connection());
        if (!$rbac->check($userId, $permission)) {
            // 跳转到错误页或返回 JSON
            return response('无权限', 403);
        }
        return $next($request);
    }
}
// 路由中使用(以Laravel为例)
Route::middleware('rbac:article.create')->post('/article', [ArticleController::class, 'store']);

性能优化要点

缓存用户权限(避免每次请求都查库)

class RbacWithCache extends Rbac
{
    public function check($userId, $permissionName)
    {
        $cacheKey = "user_permissions_{$userId}";
        // 假设使用 Redis
        $permissions = redis()->get($cacheKey);
        if (!$permissions) {
            $permissions = $this->getUserPermissions($userId);
            redis()->setex($cacheKey, 3600, json_encode($permissions)); // 缓存1小时
        } else {
            $permissions = json_decode($permissions, true);
        }
        // 如果用户是超级管理员,直接放行
        if (in_array('*', $permissions)) return true;
        return in_array($permissionName, $permissions);
    }
}

数据库索引优化

user_rolesrole_permissions 表中,为 user_idrole_idpermission_id 建立索引:

ALTER TABLE user_roles ADD INDEX idx_user_id (user_id);
ALTER TABLE user_roles ADD INDEX idx_role_id (role_id);
ALTER TABLE role_permissions ADD INDEX idx_role_id (role_id);
ALTER TABLE role_permissions ADD INDEX idx_permission_id (permission_id);

权限命名规范(最佳实践)

采用“模块.操作”格式,清晰且易于管理:

权限名 说明
user.create 创建用户
user.edit 编辑用户
user.delete 删除用户
article.create 创建文章
article.publish 发布文章(审批)
system.config 修改系统配置
report.view 查看报表

不建议使用数字 ID 作为权限标识,因为可读性差,难以维护。


完整示例:种子数据(初始化)

-- 插入角色
INSERT INTO roles (name, description) VALUES 
('super_admin', '超级管理员,拥有所有权限'),
('editor', '编辑,可管理文章'),
('viewer', '浏览者,仅可查看文章');
-- 插入权限
INSERT INTO permissions (name, label) VALUES
('article.create', '创建文章'),
('article.edit', '编辑文章'),
('article.delete', '删除文章'),
('article.view', '查看文章'),
('user.manage', '管理用户');
-- 超级管理员关联所有权限
INSERT INTO role_permissions (role_id, permission_id)
SELECT 1, id FROM permissions;
-- 编辑角色:拥有文章相关权限
INSERT INTO role_permissions (role_id, permission_id)
SELECT 2, id FROM permissions WHERE name LIKE 'article.%';
-- 浏览者:仅文章查看权限
INSERT INTO role_permissions (role_id, permission_id)
SELECT 3, id FROM permissions WHERE name = 'article.view';

常见问题及解决方案

Q1:权限检查性能太慢怎么办?

  • 增加缓存(如 Redis / Memcached)
  • 批量查询:在一次请求中获取用户所有权限,然后通过 in_array() 判断,而不是每次 check() 都查库

Q2:一个用户多个角色,权限怎么合并?

  • 上面的 SQL 已经自动做了 UNION(多对多),一个用户通过多个角色获取权限集合,只要满足任一角色有该权限即可。

Q3:如何处理“拒绝”(黑名单)和“允许”(白名单)?

  • 可以扩展为:在 user_roles 表上加一个 is_deny 字段,但更推荐的做法是:只做白名单(允许),不单独声明禁止,如果某个角色没有某个权限,那就是禁止。

Q4:前端如何根据权限动态显示菜单?

  • 后端提供一个接口 /api/user/permissions,返回该用户的所有权限列表。
  • 前端根据权限列表,动态渲染/隐藏菜单项。

一个健壮的 PHP 权限系统核心要素:

  1. 清晰的表结构(用户、角色、权限 三张主表 + 两张关联表)
  2. 缓存(减少数据库压力)
  3. 统一入口(中间件或基类控制器)
  4. 可扩展(后期支持数据权限、字段权限)

如果你需要更高级的功能(如数据级权限——某个用户只能看自己的文章),可以在上述基础上扩展,在 role_permissions 表中增加 scope 字段,或单独建立数据权限表。

抱歉,评论功能暂时关闭!