不能用 Crypt 加密用户密码,因其可逆且无盐无迭代,APP_KEY 泄露则密码全暴露;应使用 Hash::make()(基于 bcrypt)哈希密码,自动加盐和自适应迭代,验证用 Hash::check()。

为什么不能用 Crypt 加密用户密码
直接用 Crypt::encrypt() 存密码是危险操作。它可逆,一旦 APP_KEY 泄露,所有密码瞬间解出;且不带盐、不加迭代,扛不住彩虹表和暴力破解。
正确做法永远是用 Hash::make() —— 它底层调用 bcrypt(PHP 的 password_hash()),自动加盐、自适应迭代次数,验证时用 Hash::check():
use IlluminateSupportFacadesHash; $hashed = Hash::make('user_password_123'); // 生成 $2y$... 开头的哈希 Hash::check('user_password_123', $hashed); // true Hash::check('wrong', $hashed); // false
-
APP_KEY泄露不影响已哈希的密码安全 - 不要手动拼接盐或调用
md5()/sha1()—— 这些已被证明不安全 -
Hash::needsRehash($hashed)可检测是否需因算法升级重新哈希
Crypt 适合加密哪些数据
Crypt 是对称加密(AES-256-CBC),依赖 APP_KEY,适用于「需要后续解密」的敏感字段,比如:用户身份证号(合规脱敏存储)、支付渠道返回的 Token、第三方 API 密钥等。
注意:它不校验完整性,Crypt::decrypt() 解密失败会抛出 IlluminateContractsEncryptionDecryptException,必须捕获:
use IlluminateSupportFacadesCrypt; use IlluminateContractsEncryptionDecryptException; try { $decrypted = Crypt::decrypt($encrypted_value); } catch (DecryptException $e) { // 记录日志,返回默认值或报错 }
- 加密后数据是 base64 编码字符串,长度远大于原文,别存进定长字段(如
VARCHAR(20)) - 数据库迁移时若改过
APP_KEY,旧数据将永久无法解密 —— 生产环境换 key 前必须批量解密重加密 -
Crypt::encryptString()和Crypt::decryptString()专用于字符串,避免序列化开销
如何安全地在数据库中加解密字段
不要裸写 Crypt::encrypt() 在模型里。用 laravel 的 casts + 自定义 cast 更可控:
php artisan make:cast EncryptedStringCast
然后在 app/Casts/EncryptedStringCast.php 中:
namespace AppCasts; use IlluminateContractsDatabaseEloquentCastsAttributes; use IlluminateSupportFacadesCrypt; use IlluminateContractsEncryptionDecryptException; class EncryptedStringCast implements CastsAttributes { public function get($model, string $key, $value, array $attributes) { if (is_NULL($value)) return null; try { return Crypt::decrypt($value); } catch (DecryptException) { return null; // 或 throw new RuntimeException } } public function set($model, string $key, $value, array $attributes) { return is_null($value) ? null : Crypt::encrypt($value); } }
在模型中使用:
protected $casts = [ 'id_number' => AppCastsEncryptedStringCast::class, ];
- 这样读写都透明,
$user->id_number拿到的是明文,存库自动加密 - 避免在
accessor/mutator中硬编码加密逻辑,不易复用也不易测试 - 如果字段可能为空,
get()方法里必须判空,否则Crypt::decrypt(null)报错
常见错误:DecryptException 怎么排查
典型报错信息:IlluminateContractsEncryptionDecryptException: The payload is invalid.
90% 是以下原因:
-
APP_KEY不一致:本地开发用一个 key,部署时用了另一个(尤其 docker 或 Forge 部署漏配) - 数据被截断:mysql 字段类型太小(如
TEXT足够,但误设为VARCHAR(100)),入库时被砍掉末尾 - 多环境共享加密数据但未同步 key:例如 staging 和 production 共用数据库,但 key 不同
- 从 json 字段里取值后未 trim 空格或换行 ——
Crypt::decrypt()对空白敏感
快速验证方法:在相同环境下执行 dd(Crypt::encrypt('test'), Crypt::decrypt(Crypt::encrypt('test')));,看是否闭环。不通过就先查 APP_KEY。
加密不是银弹。该脱敏展示的字段(如手机号中间四位),优先用前端掩码或数据库 CONCAT 处理;真正需要可逆加解密的场景才动 Crypt —— 每多一层解密,就多一个密钥管理负担。