加密字段必须在模型中显式声明$encrypt_fields,否则即使编写加密逻辑也会明文入库;需确保字段名与数据库列名一致、类型长度适配Base64膨胀、密钥配置正确且避免静默失败。

加密字段必须在模型里显式声明 encrypt_fields
ThinkPHP 不会自动识别哪些字段要加密,也不会扫描数据库字段名做默认加解密。没声明,哪怕你写了加密逻辑,save() 时照样明文入库。
实操建议:
- 在模型类中定义
protected $encrypt_fields = ['phone', 'id_card', 'email']; - 字段名必须和数据库列名、模型属性名完全一致(区分大小写)
- 不支持嵌套数组或 JSON 字段内层字段单独加密;如需加密
extra字段里的bank_no,得先把整个extra当字符串加密,或改用手动加解密 - 若用
Db::table()而非模型操作,$encrypt_fields完全无效——这是最常踩的坑
think\facade\Crypt 加解密不能直接用于模型自动流程
模型自动加解密底层调用的是 think\model\concern\Encrypt,它封装了对 Crypt 的调用,但做了字段过滤和生命周期控制。直接拿 Crypt::encrypt() 处理字段再塞进数据,会导致重复加密或解密失败。
常见错误现象:
立即学习“PHP免费学习笔记(深入)”;
- 查出的数据是乱码或 Base64 解不开:因为
Crypt::decrypt()被模型又跑了一遍 - 新增记录后查不到:加密后字符串含特殊字符(如
+、/),被 URL 解析或数据库截断 - 使用
where('phone', 'like', '%138%')查不到:加密后内容完全不可检索,模糊查询失效
正确做法是只靠模型自动机制,不干预中间过程;如需手动加解密(比如导出场景),统一走 Crypt::encrypt($data, $key) 并确保 $key 和配置一致。
MySQL 字段类型和长度必须适配加密后字符串
AES-128-CBC 加密后,原始字符串每 16 字节会膨胀为 24 字节 Base64 编码(含填充)。10 位手机号加密后约 24 字符,身份证号(18 位)加密后约 32 字符,远超明文长度。
性能与兼容性影响:
-
VARCHAR(20)存手机号?肯定溢出——至少留VARCHAR(40) - 用
TEXT类型虽安全,但无法建唯一索引,也影响 JOIN 性能 - MySQL 5.7+ 对
utf8mb4下 Base64 字符无兼容问题,但旧版本若用utf8可能报错“Incorrect string value” - 迁移存量数据时,先加密再更新,别用
UPDATE SET phone = encrypt(phone)这种 SQL —— 没有 PHP 环境,encrypt()函数不存在
解密失败时不会抛异常,而是返回空或原始密文
ThinkPHP 的自动解密设计是“静默失败”:密钥错误、数据损坏、编码异常时,getAttribute() 默认返回空字符串或原密文字符串,而不是报错中断。线上出问题往往表现为“用户数据消失了”,排查成本很高。
容易被忽略的地方:
- 测试时用的
app_debug = true,上线关了 debug 就看不到解密日志 - 密钥硬编码在模型里?一旦换环境(如 Docker 容器重建),密钥不同就全量解密失败
- 加密用的
Crypt配置项是crypt.key,不是app.key,两个 key 写混就白忙活 - 不要依赖
__get()触发解密——如果字段被hidden或未在visible中声明,解密根本不会执行
上线前务必用非法密钥试一次,确认日志里有没有 CryptException,没有就说明失败路径被吞掉了。



















