GORM加密需严格遵循钩子签名、密文写入和解密时机三原则:钩子必须为指针接收者且参数类型精确匹配;密文须用tx.Statement.SetColumn写入而非直接赋值;解密应封装访问器或实现Scanner/Valuer,避免AfterFind污染数据。

直接在模型上写 BeforeCreate 和 BeforeUpdate 钩子就能加密,但 90% 的人卡在签名不对、密文没写进 SQL、解密污染数据这三步——加密看似跑通,实际入库的还是明文。
钩子签名必须一字不差,否则完全不触发
GORM 对钩子签名零容忍,错一个字符或类型就静默跳过,不会报错也不会提示。
-
(*User)必须是指针接收者,(u User)(值接收者)→ 不调用 -
tx *gorm.DB参数类型必须精确匹配,tx gorm.Session或tx *gorm.ConnPool→ 不调用 - 返回值只能是
error,bool、string、多返回值 → 不调用 - 方法名大小写严格:必须是
BeforeCreate,beforecreate或Beforecreate→ 不调用
加密后必须用 tx.Statement.SetColumn 写入,不能改结构体字段
直接赋值 u.Phone = encrypted 是无效的。GORM 后续仍从原始反射值取值拼 SQL,密文被覆盖回明文或空字符串。
- 正确写法:
tx.Statement.SetColumn("phone", encrypted)—— 绕过结构体映射,直插 SQL 参数列表 - 若字段是
*string或sql.NullString,先判空:if u.Phone != nil或if u.Phone.Valid,否则可能 panic - 避免在钩子里调
tx.Create()、tx.First()等操作,极易引发递归或死锁
数据库字段长度和类型必须提前适配密文膨胀
AES-GCM 加密 + base64 编码后,100 字节明文会变成约 160+ 字节;原为 VARCHAR(255) 的字段大概率报 Data too long for column。
立即学习“go语言免费学习笔记(深入)”;
- 敏感字段如
phone、id_card建议设为VARCHAR(1024),别依赖TEXT—— 它在索引、WHERE、ORDER BY 中行为不一致 - 原为
INT或UUID类型的字段,加密后必须改为字符串类型,否则插入失败 - GORM 不自动改 schema,扩容需手动执行:
ALTER TABLE users MODIFY COLUMN phone VARCHAR(1024)
解密绝不能放 AfterFind,应封装访问器或实现 Scanner/Valuer
AfterFind 在每次 SELECT 后无差别触发,且直接修改结构体字段——一旦解密赋值,后续 Save() 可能把明文错当成新值写回库,造成数据污染。
- 推荐做法:
func (u *User) GetPhone() (string, error),内部按需解密并缓存结果 - 更底层方案:让字段类型实现
sql.Scanner和driver.Valuer,GORM 在读写时自动加解密 - 密钥不能硬编码在钩子里,应由模型实例持有(如
u.Encryptor),或从tx.Statement.Context获取
最易被忽略的是密钥生命周期管理——钩子本身不提供配置注入能力,密钥若从全局变量或常量读取,测试、多租户、密钥轮换全都会出问题。真正落地时,得把加密器作为依赖注入到模型实例里,而不是让它自己去“找”密钥。


















