应使用 think\facade\crypt 替代 str::encrypt/decrypt,配合 base64 编码、足够长的字段类型及 crypt_version 版本控制实现安全可维护的字段级加密。

ThinkPHP 6 的 encrypt 和 decrypt 不该直接用在模型字段上
TP6 自带的 think\helper\Str::encrypt() 和 think\helper\Str::decrypt() 是对称加密工具,但它们默认用的是 openssl_encrypt + OPENSSL_ZERO_PADDING,密钥长度、IV 生成方式都不透明,且不支持自定义加密算法或密钥管理。直接塞进模型的 setAttr 或 getAttr 里,会导致数据无法跨环境解密(比如测试服加的密,生产服解不开),也容易和数据库字段类型(如 varchar(255))不匹配——加密后字符串可能超长或含不可见字符。
实操建议:
- 改用
think\facade\Crypt(TP6.1+ 内置),它封装了 AES-128-CBC,密钥由app.crypt_key配置控制,更可控 - 加密前先
base64_encode()原始值,避免二进制内容污染字段;解密时反向操作 - 字段类型必须设为
text或足够长的varchar(AES 加密后 base64 编码长度 ≈ 原始字节数 × 1.33 + 16 字节 IV)
在模型中用 setAttr + getAttr 实现自动加解密
这是最轻量、侵入性最小的做法,适合单字段(如手机号、身份证号)加密。关键不是“加不加”,而是“什么时候加”和“加完能不能查”。
实操建议:
- 只对写入时的值加密,读取时解密;不要在
where条件里对加密字段做等值查询(否则会把明文当密文去比) - 加密字段不能参与
order、group、like等数据库操作——加密后字符串无序且不可索引 - 示例:给
user模型的id_card字段加解密
protected function setIdCardAttr($value)
{
return $value ? \think\facade\Crypt::encrypt(base64_encode($value)) : null;
}
protected function getIdCardAttr($value)
{
return $value ? base64_decode(\think\facade\Crypt::decrypt($value)) : null;
}
敏感字段加密后没法用 where 查询?那就别查明文
很多人卡在这一步:想搜“张三的身份证号”,但字段已加密,where('id_card', 'xxx') 肯定查不到。这不是 TP 的问题,是加密设计的必然约束。
实操建议:
- 如果业务强依赖模糊查询(如查某地区身份证前6位),改用「可搜索加密」方案(如 AES-SIV 或 Hash + 盐),但 TP 没内置,得自己集成
defuse/php-encryption等库 - 更现实的做法是:把需要检索的字段(如身份证前6位、手机号前3位)单独存为明文索引字段,加
index,和加密字段一起维护 - 绝对不要为了能查,把整个加密逻辑搬到 SQL 层(比如用 MySQL 的
AES_ENCRYPT),这会让应用层彻底失去密钥控制权
密钥轮换时老数据怎么解?TP 的 Crypt 不支持多密钥版本
线上系统跑久了,总要换密钥。但 \think\facade\Crypt 只认配置文件里的 app.crypt_key,换完老数据就全废了。
实操建议:
- 加一层密钥路由:在加密字段旁加个
crypt_version小整数字段,记录当时用的密钥版本号 - 解密时先读
crypt_version,再查对应密钥(从配置、DB 或 Vault 中获取),而不是硬编码用当前app.crypt_key - 新写入一律用新版密钥,老数据按需批量迁移(后台任务跑一遍
update ... set id_card = new_encrypted, crypt_version = 2)
这事没框架帮你兜底,密钥生命周期管理永远比加解密代码本身更复杂。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!










