本文目录导读:

在 PHP 中进行权限治理(权限管理/访问控制)是一个非常核心且常见的安全与架构需求,由于 PHP 本身没有内置的、统一的权限系统,你需要根据项目规模和复杂度自行实现或使用第三方包。
下面我会从核心概念、常见实现模式、代码示例以及进阶方案四个维度来详细讲解。
核心概念:权限管理三要素
几乎所有权限系统都围绕这三个问题设计:
- Who:谁?(用户、角色)
- What:对什么?(资源,如文章、订单、API)
- How:怎么做?(操作,如增、删、改、查)
常见模型对比
| 模型 | 适用场景 | 优点 | 缺点 |
|---|---|---|---|
| ACL (访问控制列表) | 小团队、用户极少 | 简单、直接 | 用户多时管理噩梦 |
| RBAC (基于角色的访问控制) | 大多数中小型项目 | 权限与用户解耦,易扩展 | 角色粒度较粗 |
| ABAC (基于属性的访问控制) | 大型、复杂业务(如金融、OA) | 最灵活、细粒度 | 实现复杂,性能消耗较大 |
一般推荐使用 RBAC(角色-权限),这是最平衡的方案。
数据库表结构设计 (以 RBAC 为例)
你需要设计至少 4-5 张表。
-- 1. 用户表 CREATE TABLE `users` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, PRIMARY KEY (`id`) ); -- 2. 角色表 CREATE TABLE `roles` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, -- 如 admin, editor, viewer PRIMARY KEY (`id`) ); -- 3. 权限表 (定义具体操作) CREATE TABLE `permissions` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT, `name` varchar(100) NOT NULL, -- 如 article.create, user.delete `guard_name` varchar(50) DEFAULT 'web', -- 区分 web/api PRIMARY KEY (`id`), UNIQUE KEY `name` (`name`) ); -- 4. 用户-角色关联表 CREATE TABLE `role_user` ( `user_id` int(11) unsigned NOT NULL, `role_id` int(11) unsigned NOT NULL, PRIMARY KEY (`user_id`, `role_id`), FOREIGN KEY (`user_id`) REFERENCES `users` (`id`), FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ); -- 5. 角色-权限关联表 CREATE TABLE `role_has_permissions` ( `permission_id` int(11) unsigned NOT NULL, `role_id` int(11) unsigned NOT NULL, PRIMARY KEY (`permission_id`, `role_id`), FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`), FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) );
PHP 代码实现 (原生 + 面向过程示例)
用户登录后,将权限缓存到 Session
<?php
// login.php
function login($userId) {
// 1. 查询该用户的所有角色
$roles = DB::query("SELECT role_id FROM role_user WHERE user_id = ?", [$userId]);
// 2. 根据角色查询对应权限
$permissions = [];
foreach ($roles as $role) {
$perms = DB::query("SELECT p.name
FROM permissions p
JOIN role_has_permissions rp ON p.id = rp.permission_id
WHERE rp.role_id = ?", [$role['role_id']]);
foreach ($perms as $perm) {
$permissions[] = $perm['name'];
}
}
// 3. 去重并存入 Session
$_SESSION['user_permissions'] = array_unique($permissions);
}
权限检查函数
<?php
// helper.php
function can($permissionName) {
// 超级管理员跳过检查 (一般通过角色名判断)
if ($_SESSION['role'] === 'super_admin') {
return true;
}
// 检查当前用户是否有此权限
return in_array($permissionName, $_SESSION['user_permissions'] ?? []);
}
在控制器或路由中使用
<?php
// ArticleController.php
class ArticleController {
public function create() {
if (!can('article.create')) {
http_response_code(403);
die('无权限创建文章');
}
// 权限通过,继续业务逻辑
echo "创建文章成功";
}
public function delete($id) {
// 更细粒度:检查是否为自己的文章 (需要额外逻辑)
$article = Article::find($id);
if (!can('article.delete') || $article->user_id !== $_SESSION['user_id']) {
http_response_code(403);
die('无权删除');
}
// ...
}
}
进阶与最佳实践
使用 Middleware (中间件) 统一拦截
在现代 PHP 框架(Laravel、ThinkPHP、Symfony)中,推荐将权限检查放在中间件层,避免在每个控制器方法中重复写 if(can())。
Laravel 示例:
// 1. 创建中间件 CheckPermission
public function handle($request, Closure $next, $permission)
{
if (!$request->user()->can($permission)) {
abort(403);
}
return $next($request);
}
// 2. 路由中使用
Route::post('/articles', [ArticleController::class, 'store'])
->middleware('permission:article.create');
细粒度数据权限
RBAC 只能控制操作权限,如果你需要控制数据行级别(销售只能看自己的客户),需要引入数据权限规则:
- 方案一:在 SQL 查询中加入用户条件
WHERE user_id = ?。 - 方案二:使用 ABAC 或 策略模式 (Policy)。
Laravel Policy 示例:
// app/Policies/PostPolicy.php
public function update(User $user, Post $post)
{
return $user->id === $post->user_id || $user->isAdmin();
}
使用现成的权限包 (强烈推荐)
除非你是在作业或极简单的项目中,否则不建议自己手写权限系统,成熟的包已经处理好了缓存、性能优化、多 guard、丰富 API 等大量细节。
| 包名 (Composer) | 适合框架 | 特点 |
|---|---|---|
| spatie/laravel-permission | Laravel | 最流行,支持角色、权限、多 guard、缓存、Blade 指令、API 资源 |
| cartalyst/sentinel | 通用/自建框架 | 轻量、功能完整、不绑定特定框架 |
| yii2-rbac | Yii2 | Yii2 官方自带 |
学会使用一个成熟的包是更有迁移价值和性价比的做法。
常见坑与建议
- 权限尽量不要用
int位运算:- 早期常用
1,2,4,8二进制位表示权限,$permission = 7表示有增删改,这在小项目可以,但在大项目中维护性差、扩展性极差(32 位/64 位溢出),现在基本不推荐。
- 早期常用
- 缓存权限数据:
- 每次请求都查数据库读角色/权限,当用户数大时会变慢,建议登录后缓存到
Session、Redis或文件缓存。
- 每次请求都查数据库读角色/权限,当用户数大时会变慢,建议登录后缓存到
- 前后端分离 (API) 的场景:
将权限写入 Token (JWT payload) 或 通过 Redis 校验,使用中间件拦截所有 API 请求。
- 超管设计:
- 给一个特殊角色(如
super_admin),在权限检查时直接返回true,不要把所有权限都挂到这个角色上。
- 给一个特殊角色(如
- 权限名称命名规范:
- 动词 + 资源:
article.create、article.edit、user.delete - 不要用中文,容易导致编码问题。
- 动词 + 资源:
| 你的场景 | 推荐做法 |
|---|---|
| 简单 demo | 用 Session + 数组存权限名,in_array() 检查 |
| 中小型商业项目 | 使用 RBAC + 数据库,用现成包(推荐 spatie/laravel-permission) |
| 大型复杂项目 | RBAC + ABAC 混合,配合策略定义数据级别权限 |
建议先手动模拟一次基本的 RBAC 流程(用户 -> 角色 -> 权限),再使用成熟的包在生产环境。