ThinkPHP的$readonly对系统配置字段形同虚设,因其仅拦截模型save/update,无法防御Db原生操作、静态调用、强制赋值、字段名不匹配等绕过方式;且设updated_at为只读会导致时间戳静默冻结,审计失效。

ThinkPHP 的 $readonly 不能单独保护系统配置类字段,尤其当配置值需动态加载、跨请求共享或被定时任务修改时,它连“应用层防护”都算不上牢固。
只读字段在系统配置模型里为什么形同虚设
系统配置(如 sys_config 表)常被多入口写入:后台管理页、API 接口、定时任务脚本、甚至 DBA 直连 SQL。而 $readonly 只拦截模型实例的 save() 和 update(),以下情况完全绕过:
-
Db::name('sys_config')->where('key', 'site_name')->update(['value' => 'new'])—— 原生操作不走模型,$readonly不触发 -
ConfigModel::update(['value' => 'hack'], ['key' => 'admin_email'])—— 静态调用跳过模型初始化 -
$model->data($data, true)->save()—— 第二个参数true强制赋值,过滤全失效 - 字段名写成
siteName或SITE_NAME,但数据库列是site_name—— 大小写/命名风格不一致,匹配失败
系统配置该设哪些字段为只读(不是“能不能”,而是“该不该”)
系统配置表通常含 key、value、type、status、updated_at 等字段。设只读要分清语义:
-
必须设只读的:
key(主键标识,改了就指向另一条配置)、created_at(创建时间不应被覆盖) -
谨慎设只读的:
status(若支持后台开关配置项,设只读会卡住正常启停) -
别碰
value:它是核心可变字段,设只读等于锁死所有配置更新能力 -
别设
id:自增主键,框架新增时自动忽略写入,加$readonly反而可能干扰逻辑
比 $readonly 更早、更稳的拦截点
对系统配置这类高敏感数据,只靠模型层过滤等于没锁门。真正有效的组合是:
立即学习“PHP免费学习笔记(深入)”;
- 验证器里用
only(['value', 'status'])显式放行允许修改的字段,非法字段在参数解析阶段就被踢掉 - 数据库加
CHECK (key IN ('site_name', 'admin_email', 'upload_limit'))(PostgreSQL)或触发器(MySQL),让非法key写入直接报错 - 关键字段如
status封装专用方法:enable()/disable(),内部校验状态流转,不暴露裸update() - 缓存层加防穿透:查不到配置时主动写
cache:config:site_name_null(60秒过期),避免反复打库
最容易被忽略的坑:时间字段设只读后“冻住”了
如果把 updated_at 加进 $readonly,那它真不会自动更新 —— 框架不会补、不报错、也不警告。你看到的永远是旧时间戳,而业务日志却显示“已更新”。这种静默失效比报错更危险,尤其在审计场景下会误导判断。真正需要的是在 beforeUpdate 钩子里手动赋值:$model->updated_at = date('Y-m-d H:i:s');,而不是把它塞进 $readonly。



















