$readonly=['price']无法防止价格篡改,因其仅在模型save/update时过滤,对原生sql、强制赋值、字段名不匹配等完全无效;真正防护需签名验证前置+专用方法封装+数据库约束。

只读字段拦不住客户端直接改 price,$readonly 仅在模型 save() 和 update() 中过滤,对原生 SQL、强制赋值、大小写错位完全无效——真要防价格篡改,得靠签名验证 + 专用方法封装,不是靠 $readonly。
为什么 $readonly = ['price'] 拦不住恶意请求
它只是模型层“礼貌性忽略”,不是安全机制。常见失效场景:
- 客户端 POST
{"id":123,"price":"999"},后端用Db::name('order')->update()直接写库 ——$readonly根本不触发 - 用了
$model->data($data, true)->save(),第二个参数true强制跳过所有过滤 - 数据库字段是
unit_price,但$readonly写成unitPrice或UnitPrice,框架匹配失败 - 前端传了
price,但模型里没定义该字段($schema或$field不含它),$readonly无从过滤
真正防 price 篡改:签名验证必须前置
验签不是可选项,是第一道防线,必须在中间件里完成,不能等到控制器或模型层。
- 验签原文必须用原始输入:
$this->request->rawInput()(JSON)或手动拼接get()+post(),禁用param() - 拼串前
ksort()排序,每个 value 要rawurlencode(),剔除sign和timestamp字段 -
timestamp由客户端生成,服务端只允许 ±300 秒偏差;nonce存 Redis 600 秒去重 - 签名通过后,才允许进入业务逻辑;否则直接 401,不记录日志、不触发事务、不查 DB
price 字段更新必须走专用方法,不能裸调 save()
即使签名通过,也不能让 price 随意被覆盖。状态流转和金额变更必须封装校验逻辑。
- 定义
changePrice($newPrice)方法,在内部检查当前订单状态是否允许调价(如仅限“待支付”状态) - 校验新 price 是否在合理区间:
if ($newPrice 999999.99) { throw new Exception('price out of range'); } - 记录调价操作日志(谁、何时、从多少改到多少),而非只存最终值
- 避免在
beforeUpdate钩子里硬判$data['price']—— 它可能已被其他钩子修改,且无法区分是用户提交还是系统回调
数据库和模型层的兜底措施
$readonly 可以保留,但只是辅助,不是主力。重点放在更硬的防护上:
- MySQL 5.7+ 可设
price字段为GENERATED ALWAYS AS (...) STORED,或加CHECK (price >= 0.01)(注意 MySQL 8.0.16+ 才严格报错) - 模型里
$readonly只设真正不该变的字段,比如create_time、source;price 这类业务敏感字段,宁可不用$readonly,也别让它误拦正常流程 - 如果必须用
$readonly,确保字段名与数据库列名**全小写、下划线分隔、零误差**,例如数据库是unit_price,就写['unit_price'],别写['price'] - 关联模型(如商品表里的
price)被预载入后,$model->goods->price = 999再save()仍会生效 ——$readonly在主模型里对关联模型无效
最常被忽略的一点:把 price 加进 $readonly 后,后台运营调价接口就彻底废了;而签名没做全,API 层一暴露,价格字段立刻成为攻击入口。防护必须分层,不能指望一个配置解决所有问题。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











