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

AI摘要
【知识分享】文章讲解 Web 开发中水平越权风险,指出在业务代码中手写属主判断易漏难维护,介绍 PHP-Casbin 的 ABAC 能力:可将用户与数据对象直接传入 enforce(),通过配置文件中的 Matcher 完成对象级权限判定,使控制器代码更简洁,规则调整无需改动业务逻辑,并说明其对 ORM 模型、DTO 等对象的属性自适应读取支持。

一行代码防水平越权:用 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.idr.sub.role
  • r.obj 就是被操作的具体数据,可以直接传查出来的订单对象,引擎会自动比对 r.obj.owner_idr.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 这种对象级属性判决能力,把权限规则关进配置文件的笼子里,既堵死了水平越权的漏洞,又能让自己少加几个毫无意义的排查夜班。

原文:tech1024.com/original/5216

本作品采用《CC 协议》,转载必须注明作者和本文链接
专注于分享 Go、Java、Python、PHP、Node.js 等全栈技术开发领域知识,欢迎关注我的 技术圈
讨论数量: 0
(= ̄ω ̄=)··· 暂无内容!

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!
未填写
文章
31
粉丝
11
喜欢
87
收藏
72
排名:345
访问:2.8 万
私信
所有博文
社区赞助商