模型setAttr/getAttr是唯一靠谱的字段级加密入口:TP6不支持自动识别敏感字段,$encrypt_fields已失效;必须重写set{Field}Attr和get{Field}Attr实现自动加解密,禁用md5/password_hash等不可逆算法,密钥从配置读取,IV需固定衍生,数据库字段预留足够长度,查询须先加密条件或采用“明文索引+密文存储”双写方案。

模型 setAttr/getAttr 是唯一靠谱的字段级加密入口
ThinkPHP 6 不支持自动识别敏感字段并加密,$encrypt_fields 是 TP5 的遗留概念,TP6 中已彻底失效。想让手机号、身份证号写入数据库前自动加密,唯一稳定可控的方式是重写模型的 set{Field}Attr 和 get{Field}Attr 方法。
常见错误包括:在控制器里手动调用加密函数再赋值(绕过修改器)、用 Db::table() 直接操作(完全不触发)、或把逻辑塞进 saving 事件(批量更新或原生 SQL 时照样漏掉)。
-
setPhoneAttr($value)必须判空:if (empty($value)) { return ''; },否则 null 会被加密成乱码 - 加密必须用可逆算法,禁用
md5()、password_hash()—— 这些解不开,字段就废了 - 密钥不能硬编码,应从配置读取:
config('app.aes_key'),且确保长度为 16 或 32 字节(对应 AES-128 / AES-256) - IV 不能复用,但也不能每次随机——否则无法解密;推荐用
substr(hash('sha256', $key), 0, 16)衍生固定 IV - 数据库字段类型必须设为
VARCHAR(255)或TEXT,并使用utf8mb4_bin排序规则,避免 Base64 密文被截断或乱码
where 查询加密字段时别直接比明文
加密后字段值每次不同(哪怕 IV 固定,算法本身也含随机填充),所以 where('phone', '13812345678') 永远查不到数据——因为库里存的是密文,不是明文。
线上最常翻车的写法是:开发时本地密钥一致,能查;上线后密钥一换,所有查询全挂,还查不出原因。
立即学习“PHP免费学习笔记(深入)”;
- 要查,就得先手动加密条件:
where('phone', encrypt('13812345678', $key, $iv)),且加密逻辑必须和setPhoneAttr完全一致 - 更稳妥的做法是「明文索引 + 密文存储」双写:比如存
phone_md5(用于查)、phone_enc(用于展示),前者建索引,后者只读 - 禁止对加密字段用
LIKE或模糊匹配——AES 加密后无规律,LIKE '%138%'纯属碰运气 - 如果要用 Redis 缓存做查询加速,建议存
md5(phone) => user_id,而不是加密后的 phone
缓存加密必须用 sodium_crypto_secretbox()
think\facade\Crypt 和 openssl_encrypt('AES-CBC') 都不适合缓存场景。前者是为 Cookie 设计的,密钥派生弱;后者无认证,密文被篡改后仍能解出垃圾数据,你根本察觉不到。
正确姿势是 PHP 内置的 sodium_crypto_secretbox():它自带完整性校验(AEAD),API 简单,且 nonce 生成和密钥管理有标准方式。
- 密钥必须用
sodium_crypto_secretbox_keygen()生成,32 字节,不能截、不能补、不能用md5()凑 - 每次加密都要新 nonce:
sodium_randombytes_buf(24),长度固定为 24 字节 - 密文是二进制,必须
base64_encode()后再存;解密失败返回false,不能忽略,得显式判断 - nonce 必须和密文一起存,常见做法是拼接:
base64_encode($nonce . $ciphertext),解密时先截出前 24 字节 - 环境变量里的密钥要是原始二进制,读取时得
base64_decode(env('CACHE_KEY')),不能直接传 hex 字符串
日志和导出环节最容易泄露敏感信息
Log::write($request->param(), 'info') 这行代码跑一次,密码、token 就全躺在日志文件里了。这不是框架漏洞,是你没做脱敏。
同样,Excel 导出时如果直接 $user->toArray(),加密字段可能被意外暴露——尤其当模型没配 hidden 或访问器没生效时。
- 日志必须用
Log::record()+ 递归脱敏函数,关键词匹配要大小写不敏感(password、PWD、Token都得覆盖) - 脱敏值统一用
***,不能留空字符串,否则可能触发空值校验或 JSON 解析异常 - 导出原始手机号时,TP6.1+ 可用
$user->getAttr('mobile', null, false)绕过getMobileAttr(),但仅限明确需要明文的场景 - 模板输出或 API 返回时,
hidden属性只影响toJson(),不影响toArray();要彻底过滤,得重写toArray()或用visible - CLI 脚本、队列任务、定时任务这些非 HTTP 场景,一样会打日志、查数据库、导出数据——脱敏逻辑必须全局一致,不能只在控制器里写一遍



















