$readonly不能防止物流状态被前端修改,它仅在模型save/update时过滤字段,而恶意请求可直连数据库、跳过过滤或因命名不一致失效;真正防线是签名验证+专用状态流转方法+数据库check约束。

$readonly 不能防止物流状态被前端修改,它只在模型 save()/update() 中过滤字段,而前端提交的非法状态早已绕过它进入数据库——真正防线是签名验证 + 专用状态流转方法 + 数据库 CHECK 约束。
为什么 $readonly = ['logistics_status'] 完全拦不住恶意请求
它不是安全机制,只是模型层的数据组装过滤。常见绕过方式包括:
- 用
Db::name('order')->update(['logistics_status' => 99])直连数据库,$readonly 根本不触发 - 调用
$order->data($data, true)->save(),第二个参数true强制跳过所有过滤(含 $readonly) - 数据库字段是
logistics_status,但 $readonly 写成logisticsStatus或LOGISTICS_STATUS,框架字符串精确匹配失败 - 前端 POST
{"id":123,"logistics_status":"delivered"},后端却用$this->request->param()接收——该方法会自动类型转换和 urldecode,导致验签原文与客户端不一致,签名失效后所有防护形同虚设
真正防物流状态篡改:三道不可省略的防线
单靠 $readonly 上线等于裸奔。必须分层落地:
- 中间件里完成签名验证:
$this->request->rawInput()取原始 JSON,ksort()排序参数,逐个rawurlencode()value,剔除sign和timestamp后拼串验签;timestamp允许 ±300 秒偏差,nonce存 Redis 600 秒去重 - 签名通过后,状态变更必须走专用方法:
$order->changeLogisticsStatus('shipped'),内部校验当前状态是否允许流转(如仅允许pending → shipped,禁止反向或跨级)、检查操作人权限、记录审计日志 - 数据库加硬约束:
CHECK (logistics_status IN ('pending', 'shipped', 'delivered', 'cancelled'))(MySQL 8.0.16+ 严格报错),让非法 UPDATE 直接失败
哪些字段该放进 $readonly?别乱设
$readonly 的真实作用是防止业务逻辑中误覆盖,不是拦截前端。设错反而埋坑:
- 必须设只读:
create_time、delete_time、source(如'express'或'self_pickup')——这些写入即定型 - 谨慎设只读:
user_id、order_no—— 如果后台支持转单、补单,设只读会卡死流程 - 别碰
id和update_time:id新增时框架自动忽略,加 $readonly 可能干扰 UUID;update_time若放进 $readonly,它就真不会自动更新了,且框架不报错、不提醒,数据悄悄“冻住”
最容易被忽略的关联场景
预载入 with('logistics') 后,$order->logistics->status = 'delivered'; $order->logistics->save(); 这种写法完全不受主模型 $readonly 控制。解决方案只有两个:
- 在
Logistics模型的setStatusAttr()方法里拦截:if ($this->exists && $value !== $this->origin('status')) { throw new Exception('logistics status is readonly'); } - 彻底避免修改预载入对象,改用显式更新:
Logistics::where('order_id', $order->id)->update(['status' => 'shipped'])
真正危险的不是 $readonly 失效,而是你把它当成了安全锁——它连门把手都算不上,顶多是一张贴纸。物流状态这种高敏感字段,签名验签没做稳,后面所有逻辑都是空中楼阁。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











