一行代码防水平越权:用 PHP-Casbin 终结业务里的数据级 if-else

做 Web 开发,最怕安全部门突然发来一张漏洞工单:接口存在“水平越权”。
通俗点说,张三登录系统后,本来只能查看自己的订单 id=1001,结果他在浏览器里顺手把 URL 改成了 id=1002,李四的收货地址和订单详情就全被看光了。更有甚者,直接发个 DELETE 请求,把王五的数据给抹掉了。
很多团队一开始对付这种越权,全靠在业务代码里人肉写 if-else。但随着项目越做越大,业务代码直接被各路“补丁”淹没,改一处漏三处。其实,PHP-Casbin 原生就支持把整个数据对象直接丢给它判决,一行代码就能把数据越权挡在门外。
业务代码里的防越权补丁有多痛苦
看看没有统一权限工具时,控制器里那些熟悉的“面条代码”:
// 业务控制器里随处可见的属主防御
public function update(Request $request, int $orderId)
{
$order = Order::findOrFail($orderId);
$user = auth()->user();
// 各种属主判定与业务状态强行搅在一起
if ($user->role !== 'admin' && $order->owner_id !== $user->id) {
throw new ForbiddenException("你想改别人的订单?没门");
}
if ($order->status === 'archived' && $user->role !== 'admin') {
throw new BusinessException("订单都归档了,谁也别想动");
}
$order->update($request->validated());
}
这种写法刚开始挺省事,但时间一长就会让人抓狂:
- 到处漏风:改订单要防越权,查详情要防,下载合同要防,取消订单还要防。十几个接口都得复制粘贴这一套,只要新来的同事少写一行判断,线上就是一个高危越权。
- 改规则像扫雷:产品经理突然提个需求:“质检员在工作时间内可以临时修改订单”。这时候就得把所有相关的业务控制器翻出来,逐个在
if后面追加条件,稍有不慎就漏掉一个地方。 - 业务逻辑喧宾夺主:核心业务明明只是更新几个字段,结果一大半代码全在干“查户口”和“比对身份”的杂活。
破除误区:谁说 Casbin 只能传字符串
很多用过 Casbin 的开发者,印象中它的用法都是这样的:
// 经典的路由拦截用法
$enforcer->enforce('alice', '/api/orders', 'POST');
这让人产生了一种错觉:Casbin 好像只能接纯字符串,只能在网关或中间件里判断“用户能不能调这个接口”,碰上“用户能不能改这条具体数据”就无能为力了。
其实 Casbin 的 PERM 模型设计非常灵活。在 PHP-Casbin 里,enforce() 完全可以直接传入一个真实的 PHP 对象。引擎在跑匹配规则时,能自己伸手去读取对象肚子里的属性值并进行实时判定。
规则配置:把判断逻辑从代码抽离出来
只需定义一个只有几行配置的 abac_model.conf 文件,就能把所有权规则和限制条件收得服服帖帖:
[request_definition]
r = sub, obj, act
[policy_definition]
p = sub, obj, act
[policy_effect]
e = some(where (p.eft == allow))
[matchers]
m = r.sub.role == "admin" || (r.sub.id == r.obj.owner_id && r.act in ["read", "update"] && r.obj.status != "archived")
这段配置看着短,翻译成大白话其实清清楚楚:
r.sub就是当前操作人,可以直接传当前登录用户的对象,引擎会自动去拿r.sub.id和r.sub.role。r.obj就是被操作的具体数据,可以直接传查出来的订单对象,引擎会自动比对r.obj.owner_id和r.obj.status。m是终极裁判法官:如果是系统管理员,直接一路绿灯;普通人想操作,必须满足两个硬条件——必须是自己的数据,而且订单不能处于归档状态。
控制器实战:让业务代码回归清爽
有了这套规则,再来看业务控制器里的代码,画风瞬间就变了:
use Casbin\Enforcer;
class OrderController
{
public function update(Request $request, int $orderId, Enforcer $enforcer)
{
$order = Order::findOrFail($orderId);
$user = auth()->user();
// 核心就这一行:把人、数据对象、动作全抛给 Casbin
if (!$enforcer->enforce($user, $order, 'update')) {
abort(403, "没权限动这笔数据,或者当前状态不允许");
}
// 业务逻辑清清爽爽,专心做核心流转
$order->update($request->validated());
return response()->json(['message' => '搞定']);
}
}
控制器再也不用关心“谁是谁的谁”这种复杂的亲戚关系了。它只负责把用户对象和数据模型往引擎里一送,拿到 false 就直接报 403。
即使明天产品经理要加新规则,比如“允许售后主管编辑”,也只要在配置文件里的 Matcher 追加一句条件,所有的业务控制器一行都不用动。
动态属性自适应:原生吃下各种 PHP 对象
有些开发者可能会担心:我的项目用的是 Laravel Eloquent,属性都是靠 __get() 动态取出来的;或者同事用的是自己封装的强类型 DTO,Casbin 认得出来吗?
答案是完全不需要操心。PHP-Casbin 在解析规则时,天生支持自适应内省:
// 无论是强类型 DTO、stdClass 还是 ORM 模型,随拿随用
$user = auth()->user();
$order = Order::findOrFail($orderId);
// 引擎会自动穿透对象的公有属性、Getter 方法以及魔术属性
$enforcer->enforce($user, $order, 'update');
不管是普通的 PHP 实体、动态数据对象,还是带魔术特性的框架模型,引擎都能顺畅读取其中的字段。开发者根本不需要在调用前手动把对象转成数组或拼成 JSON 字符串,怎么顺手就怎么传。
写在最后
权限拦截如果只停留在“能不能访问接口”,那就好比小区保安只在入户大门查了一次健康码,进了单元楼之后哪家哪户的门都不锁。
别再在业务逻辑里堆砌那些又臭又长、改起来胆战心惊的属主判断了。利用 PHP-Casbin 这种对象级属性判决能力,把权限规则关进配置文件的笼子里,既堵死了水平越权的漏洞,又能让自己少加几个毫无意义的排查夜班。
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
推荐文章: