gorm字段加密需严格遵循钩子签名、setcolumn写入、密文长度适配及解密隔离四原则:beforecreate/update必须用指针接收者和*gorm.db参数返回error;须用tx.statement.setcolumn写密文;敏感字段改varchar(1024);解密应封装访问器或实现scanner/valuer,禁用afterfind。

直接用 GORM 的 BeforeCreate 和 BeforeUpdate 钩子做字段加密是可行的,但绝大多数人栽在签名不匹配、写入方式错误、密钥管理失控这三处——加密逻辑看似跑通,实际从未生效,或解密后数据被二次污染。
钩子签名必须严格匹配 (*Model) BeforeCreate(tx *gorm.DB) error
GORM 不做任何容错:接收者不是指针、参数类型不是 *gorm.DB、返回值不是 error,方法就彻底静默不调用。
- 错例:
func (u User) BeforeCreate(...)(值接收者)→ 不触发 - 错例:
func (u *User) BeforeCreate(tx gorm.Session) ...(类型不对)→ 不触发 - 错例:
func (u *User) BeforeCreate(tx *gorm.DB) bool(返回非error)→ 不触发 - 正例:
func (u *User) BeforeCreate(tx *gorm.DB) error(且仅此一种)
加密后必须用 tx.Statement.SetColumn("field", encrypted) 写入
直接改结构体字段(如 u.Phone = encrypted)是无效的。GORM 后续仍会从原始反射值取值拼 SQL,密文被覆盖回明文或零值。
-
tx.Statement.SetColumn绕过结构体映射,把值直插进 SQL 参数列表,这才是唯一可靠写法 - 若字段是
*string或sql.NullString,先判空再加密,否则可能 panic - 避免在钩子里调
tx.Save()或其他查询操作,极易引发递归或死锁
数据库字段长度和类型必须提前适配密文膨胀
AES-GCM 加密 + base64 编码后,100 字节明文会变成约 136 字节;加上 12 字节 nonce 和 16 字节 auth tag,总长轻松超 160 字节。VARCHAR(255) 在多数场景下已不够用。
- 敏感字段如
phone、id_card建议设为VARCHAR(1024),而非依赖 TEXT(TEXT 在索引、WHERE 条件、排序上行为不一致) - 原为
INT或UUID类型的字段,加密后必须改为字符串类型,否则插入失败 - GORM 不自动改 schema,扩容需手动执行
ALTER TABLE ... MODIFY COLUMN ...
解密绝不能放 AfterFind,应封装访问器或实现 Scanner/Valuer
AfterFind 无差别触发,且直接修改结构体字段——一旦解密赋值,后续 Save() 可能误把明文当新值写回库,造成数据污染。
- 推荐做法:定义
func (u *User) GetPhone() (string, error),内部按需解密并缓存结果 - 更底层方案:让字段类型实现
sql.Scanner和driver.Valuer,GORM 在序列化/反序列化时自动加解密,天然支持密钥轮换 - 若硬要用
AfterFind,必须通过tx.Statement.Context显式传入开关,否则无法控制解密时机
真正难的不是写加密函数,而是确保密钥不硬编码、nonce 每次随机、密文不被截断、解密不污染原始状态——这些点任何一个松动,整个加密链路就形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











