正确做法是拆成users、roles、permissions、user_roles、role_permissions五张表,用join查询加载权限到$_session['permissions']索引数组,并在每层(模板、api、文件)独立校验。

$_SESSION['permissions'] 必须是索引数组,不是字符串、不是关联数组、不能含空格或大小写错误——否则 in_array() 会静默失败,权限校验形同虚设。
数据库设计为什么不能用 users.permission 字段存 JSON 或逗号分隔
这种设计看似省事,实则埋下三类隐患:
- 无法建立外键约束,
permissions表变更后,用户表里的权限字段完全不受控 - 查某个权限被哪些用户拥有时,得用
LIKE '%post:delete%',MySQL 无法走索引,数据量一过万就卡住 - 审计权限分配记录时,没有时间戳、操作人、变更前值等字段,出问题根本没法回溯
正确做法是拆成五张表:users、roles、permissions、user_roles、role_permissions。其中 user_roles 和 role_permissions 必须是独立表,不能合并或省略——一个用户可能同时有 editor 和 reviewer 角色,一个角色也可能对应几十个原子权限。
登录后加载权限到 session 的关键动作
不是“查完角色再查权限”,而是用一次 JOIN 查询直接拿到所有权限标识:
SELECT DISTINCT p.name FROM permissions p JOIN role_permissions rp ON p.id = rp.permission_id JOIN user_roles ur ON rp.role_id = ur.role_id WHERE ur.user_id = ?
然后必须做三件事:
- 用
array_column($rows, 'name')提取纯权限名数组,确保是[0 => 'post:edit', 1 => 'user:delete']这种格式 - 写入
$_SESSION['permissions']前,先调用session_start(),且页面顶部不能有任何输出(包括 UTF-8 BOM) - 同步设置
$_SESSION['permissions_updated_at'] = time(),后续可结合中间件做 15 分钟自动刷新,避免角色变更后 session 滞后
can() 函数为什么总返回 false
90% 的失效不是逻辑错,而是上下文没对齐:
- 函数里直接读
$_SESSION['permissions'],但调用前没确认 session 是否已启动,或 session 文件被意外清空 - 传参用了
'post:edit '(末尾带空格)或'Post:Edit'(大小写不一致),而数据库里存的是'post:edit' - 在 AJAX 请求里调用,但没检查
$_SERVER['HTTP_X_REQUESTED_WITH'] === 'XMLHttpRequest',导致 header() 重定向失败,错误被静默吞掉 - 前端把权限名硬编码在按钮
data-permission="user:delete"里,但后端校验时却用can('delete_user')——命名不统一,匹配必然失败
权限校验最容易被忽略的执行层
很多人只在控制器里加 if (!can('xxx')) die(),但漏掉三个关键点:
- 模板层:HTML 中的按钮、链接、表单提交地址,必须用
can()控制渲染,否则攻击者直接构造 POST 请求就能绕过 - API 层:每个接口入口(如
api/delete-user.php)都得单独校验,不能依赖前端传来的role=admin参数 - 文件层:上传后的附件访问路径(如
/uploads/report_20260512.pdf)也要走权限钩子,否则用户能通过 URL 猜解越权下载
真正安全的权限系统,不是靠一个 can() 函数撑起来的,而是每层都各自校验、互不信任。最脆弱的环节,往往不在你写的代码里,而在你“以为已经控制住”的地方。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











