BLOG
RBAC权限系统如何避免角色判断写满代码
代码应判断具体权限,不应到处硬编码“是不是管理员”。
权限判断的粒度,决定了 RBAC 系统是越用越清晰,还是越改越乱。很多后台项目一开始只判断“是不是管理员”,等业务长出 VIP、编辑、运营这些角色时,才发现每个按钮都要加一层角色判断,代码里到处是 if ($user->role === 'admin')。这类硬编码短期内能跑,但每新增一个角色,就要回头翻所有模板和控制器,漏改一处就是越权漏洞。
先确认要拦截的是角色还是权限
角色是身份的集合,权限是动作的许可。用户是“管理员”这个事实本身没有意义,有意义的是他能执行“编辑文章”“删除评论”“导出用户数据”这些具体操作。判断时应该问“这个用户有没有 posts.edit 这个权限”,而不是“这个用户是不是管理员”。
原因在于角色会变。今天你是管理员,明天系统拆出超级管理员和运营管理员,你的角色 ID 变了,但你能做的事情可能没变。反过来,如果代码里写死了“管理员可以删除”,将来给某个非管理员角色开放删除权限时,就得去改逻辑,而不是改配置。
所以建表时至少要有三张:用户表、角色表、权限表,再加上用户-角色、角色-权限两张关联表。用户登录后,一次性把他的权限标识数组取出来,存进 Session 或缓存。后续所有判断都基于这个数组,不再查数据库。
核心逻辑:把判断收敛到一个入口
避免角色判断写满代码的关键,是让业务代码里不出现角色名称,只出现权限标识。下面是一个可以直接用的原生 PHP 示例,假设权限已经存在 $_SESSION['permissions'] 里:
```php
<?php
function can(string $permission): bool
{
return in_array($permission, $_SESSION['permissions'] ?? [], true);
}
// 业务代码里这样用
if (!can('posts.edit')) {
http_response_code(403);
exit('你没有执行此操作的权限');
}
// 继续执行编辑逻辑
```
这个函数的价值在于把“权限从哪里来、怎么比对”收进了一个地方。以后权限来源从 Session 改成 Redis,或者从数组改成数据库查询,只改 can() 内部即可,业务代码一行不用动。
更严格的做法是做一个中间件或前置拦截器,在路由分发时统一检查。比如在入口文件里解析当前请求对应的权限标识,然后调用 can() 判断,不通过就直接返回 403,不进控制器。这样即使某个控制器忘了写判断,请求也到不了业务逻辑那一层。
按钮和菜单的显示判断
后端拦截只是底线,前端还得决定“这个按钮要不要渲染出来”。常见错误是在模板里写:
```php
<?php if ($user->role_id === 1): ?>
<a href="/admin/delete.php?id=<?= $id ?>">删除</a>
<?php endif; ?>
```
这段代码的问题很明显:角色 ID 1 是什么角色,看代码的人得去查表;将来角色结构调整,这里也要跟着改。正确做法是沿用同一个权限标识:
```php
<?php if (can('posts.delete')): ?>
<a href="/admin/delete.php?id=<?= $id ?>">删除</a>
<?php endif; ?>
```
注意一个原则:前端隐藏按钮只是改善体验,真正的安全边界在后端。就算按钮没渲染,懂技术的人直接构造 URL 也能发起请求,所以控制器里的 can() 检查绝不能省。前端判断和后端判断用的是同一个权限标识,两处保持一致,就不会出现“按钮看得到但点了报 403”的割裂感。
容易出错的三处细节
第一处是权限标识的命名。建议统一用“模块.动作”的格式,比如 user.create、order.export。不要用 can_delete 这种动词开头的命名,也不要一个权限标识管两件事。命名一旦混乱,维护者很难判断某个标识到底控制什么,最后只能靠猜。
第二处是超级管理员的处理。超级管理员通常拥有全部权限,但不要在他登录时把所有权限标识都塞进 Session,那样数据量会很大。更稳妥的做法是在 can() 函数里加一个判断:如果当前用户的角色标记为超级管理员,直接返回 true。这个角色标记可以存在用户表里,也可以单独配置。
第三处是角色与权限变更后的缓存刷新。如果用户的权限数组存在 Session 里,管理员给某个角色加了权限,已登录的该角色用户不会立即生效,必须重新登录才能拿到新权限。如果系统要求权限变更实时生效,就得把权限缓存放到 Redis 并按用户或角色做键,变更时主动删除对应缓存。个人项目或后台管理系统用 Session 足够,实时性要求高才需要引入 Redis。
检查权限系统是否可靠的几个方法
写完权限逻辑后,不要只用自己的管理员账号点一遍。按下面几个方向做检查:
第一,用无权限账号访问受保护的 URL,确认返回 403 而不是跳转到首页或直接报错。响应码和错误页面都要明确,不能让用户以为操作成功了。
第二,检查数据库里是否有越权写入。比如普通用户尝试修改他人文章,接口返回 403 的同时,数据库里那篇文章的 updated_at 不应有变化。
第三,确认按钮显示和后端拦截用的是同一个权限标识。可以临时把一个角色的某个权限去掉,刷新页面看按钮是否消失,再直接访问 URL 看是否被拦截。
第四,检查日志。关键操作如删除、导出、修改权限,应该记录操作者 ID、操作时间和请求 IP。权限系统本身也要有日志,否则出问题时无法追溯是谁在什么时间获得了什么权限。
权限系统的维护边界
权限标识的数量需要控制。如果每个按钮都定义一个权限,权限表会膨胀到难以管理。一般原则是:同一类操作共用一个权限标识,比如“文章管理”对应 posts.edit,覆盖新增、修改、删除,而不是拆成 posts.create、posts.update、posts.delete 三个。粒度太细,配置角色时反而无从下手。
另外,权限系统只解决“能不能做”的问题,不解决“做了之后合不合法”的问题。比如一个用户有 order.refund 权限,但他只能退款自己创建的订单,这就需要业务逻辑里再加一层数据归属判断。权限系统管动作,数据范围管对象,两者配合才能构成完整的访问控制。
最后留一份简短记录:这次加了哪些权限标识、对应哪些角色、改了哪个控制器或模板。半年后系统需要扩展时,这份记录能直接告诉你当时的设计意图,比翻代码猜快得多。
评论
登录后可发表评论。