加密字段必须用casts配置,不能靠accessor/mutator手动处理;正确做法是在模型中声明$casts=['ssn'=>'encrypted'];字段类型须设为text,不支持数据库查询,换密钥需手动迁移解密重加密。

加密字段必须用 casts 配置,不能靠 accessor/mutator 手动处理
手动在 get*/set* 方法里调用 encrypt() 和 decrypt() 看似可行,但会破坏 Eloquent 的变更检测、批量赋值、序列化和 JSON 输出逻辑。比如 $user->save() 可能不触发更新,$user->toArray() 返回的是密文而非明文,API 直接暴露密文还可能被误用。
正确做法是在模型中声明 casts:
protected $casts = [
'ssn' => 'encrypted',
'api_token' => 'encrypted:openssl',
];
encrypted 是 Laravel 9.2+ 内置的 cast 类型,底层走 Illuminate\Encryption\Encrypter;加冒号指定驱动(如 openssl)可切换算法,但默认已够用。
- 只对数据库字段生效,内存中属性(如
$user->temp_token)不会被自动加密 - 加密后字段类型建议设为
text(避免varchar(255)截断) - 如果字段允许为
null,加密后存的是null,不是空字符串或加密后的null
数据库迁移要配 text 字段,别用 string
Laravel 默认加密输出是 base64 编码 + IV + 加密数据,长度远超原始值。用 $table->string('ssn') 建表,存入 "123-45-6789" 这种 11 位字符串后,实际写入可能达 200+ 字符,直接报 SQLSTATE[22001]: String data, right truncated 错误。
迁移里必须显式用 text:
$table->text('ssn')->nullable();
-
mediumtext或longtext不必要,text足够(65KB) - 如果已有字段是
string,改字段类型需用DB::statement()或原生 SQL,change()在 MySQL 低版本可能失败 - 加密字段不支持数据库层面的索引或 where 查询(比如
where('ssn', '123-45-6789')查不到),这是设计使然,别硬套
encrypted cast 不支持搜索,查敏感字段得靠解密后内存过滤
数据库里存的是密文,where('ssn', $input) 永远不匹配——除非你把输入也加密再查,但这等于把密钥暴露给查询条件,完全失去加密意义。
真要按加密字段查(比如后台搜用户社保号后四位),只能:
- 查出所有记录(或加其他可索引条件缩小范围),在 PHP 层用
decrypt($user->ssn)解密后比对 - 或另建一个不可逆的哈希字段(如
ssn_hash存hash_hmac('sha256', $ssn, config('app.key'))),用于精确匹配 - 绝不把解密逻辑放进
whereRaw或数据库函数里——密钥进 SQL 日志或慢查日志就全完了
注意:Laravel 的 decrypt() 在密文损坏或密钥不匹配时抛 DecryptException,线上务必 try-catch,否则整个列表页崩掉。
换密钥后旧数据无法自动解密,迁移得手动处理
一旦改了 APP_KEY,所有用旧密钥加密的字段立刻变成“死数据”:decrypt() 全报错,getAttribute('ssn') 返回 null,且没警告。
上线新密钥前必须做数据迁移:
- 用旧密钥起一个临时 Laravel 实例(改
.env临时回滚APP_KEY) - 逐条查出加密字段,
decrypt()后再用新密钥encrypt()写回 - 别图省事用 raw SQL 替换——加密结构含 IV 和序列化头,直接字符串替换必炸
- 小数据量可命令行跑,大数据量得用 chunk + queue,避免超时
这事没法自动化兜底,密钥轮换永远是最容易漏掉的加密运维环节。











