本文目录导读:

在PHP项目中实现角色权限(RBAC,基于角色的访问控制)是一个核心的安全设计问题,下面我会从数据库设计、核心逻辑到代码实现,提供一个完整且可落地的方案。
核心概念
在开始编码前,需要明确三个关键概念:
- 用户(User):系统的使用者。
- 角色(Role):权限的集合,管理员”、“编辑”、“普通用户”。
- 权限(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_roles 和 role_permissions 表中,为 user_id、role_id、permission_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 权限系统核心要素:
- 清晰的表结构(用户、角色、权限 三张主表 + 两张关联表)
- 缓存(减少数据库压力)
- 统一入口(中间件或基类控制器)
- 可扩展(后期支持数据权限、字段权限)
如果你需要更高级的功能(如数据级权限——某个用户只能看自己的文章),可以在上述基础上扩展,在 role_permissions 表中增加 scope 字段,或单独建立数据权限表。