GORM 敏感数据加密存储
GORM 通过模型钩子 BeforeCreate 和 AfterFind 实现字段级自动加解密,确保敏感数据落盘加密、读取解密。
加密机制概述
字段级加密流程
Go
type User struct {
ID uint `gorm:"primaryKey"`
Name string
EncryptedID string `gorm:"column:encrypted_id;type:varchar(256)"` // 加密字段
PlainID string `gorm:"-:all"` // 明文字段,不映射数据库
}
AES 加密实现
加解密工具函数
Go
import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"io"
)
var aesKey = []byte("32-byte-encryption-key-here!!") // 必须 16/24/32 字节
func Encrypt(plainText string) (string, error) {
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
nonce := make([]byte, aesGCM.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
cipherText := aesGCM.Seal(nonce, nonce, []byte(plainText), nil)
return base64.StdEncoding.EncodeToString(cipherText), nil
}
func Decrypt(cipherText string) (string, error) {
block, err := aes.NewCipher(aesKey)
if err != nil {
return "", err
}
aesGCM, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
data, err := base64.StdEncoding.DecodeString(cipherText)
if err != nil {
return "", err
}
nonceSize := aesGCM.NonceSize()
nonce, cipherBytes := data[:nonceSize], data[nonceSize:]
plainText, err := aesGCM.Open(nil, nonce, cipherBytes, nil)
return string(plainText), err
}
模型钩子实现
BeforeCreate 自动加密
Go
func (u *User) BeforeCreate(tx *gorm.DB) error {
if u.PlainID != "" {
encrypted, err := Encrypt(u.PlainID)
if err != nil {
return err
}
u.EncryptedID = encrypted
u.PlainID = "" // 清空明文
}
return nil
}
// 创建时自动加密
user := &User{Name: "张三", PlainID: "110101199001011234"}
db.Create(user)
// 数据库存储:EncryptedID = "base64加密后的密文"
AfterFind 自动解密
Go
func (u *User) AfterFind(tx *gorm.DB) error {
if u.EncryptedID != "" {
decrypted, err := Decrypt(u.EncryptedID)
if err != nil {
return err
}
u.PlainID = decrypted
}
return nil
}
// 查询时自动解密
var user User
db.First(&user, 1)
// user.PlainID = "110101199001011234"(已解密)
更新操作加密
BeforeUpdate 钩子
Go
func (u *User) BeforeUpdate(tx *gorm.DB) error {
if u.PlainID != "" {
encrypted, err := Encrypt(u.PlainID)
if err != nil {
return err
}
u.EncryptedID = encrypted
u.PlainID = ""
}
return nil
}
// 更新时自动加密
db.Model(&user).Where("id = ?", 1).Update("plain_id", "new-id-value")
密钥管理
环境变量注入密钥
Go
func LoadEncryptionKey() error {
key := os.Getenv("AES_ENCRYPTION_KEY")
if key == "" {
return errors.New("AES_ENCRYPTION_KEY not set")
}
if len(key) != 16 && len(key) != 24 && len(key) != 32 {
return errors.New("key length must be 16, 24, or 32 bytes")
}
aesKey = []byte(key)
return nil
}
注意: 加密密钥严禁硬编码,必须通过环境变量或密钥管理服务注入。定期轮换密钥需处理历史数据重加密。
加密字段查询
加密后匹配查询
Go
// 查询加密字段需先加密查询条件
func FindByEncryptedID(db *gorm.DB, id string) (*User, error) {
encrypted, err := Encrypt(id)
if err != nil {
return nil, err
}
var user User
err = db.Where("encrypted_id = ?", encrypted).First(&user).Error
return &user, err
}
注意: 加密字段无法使用范围查询和模糊匹配,适合精确匹配场景。需要模糊查询时应采用其他方案(如脱敏索引)。
要点总结
- 使用 AES-GCM 算法对敏感字段加密,确保数据落盘安全
BeforeCreate和BeforeUpdate钩子负责自动加密AfterFind钩子负责自动解密,业务无感知使用明文- 加密密钥通过环境变量注入,禁止硬编码
- 加密字段仅支持精确匹配查询,不支持范围或模糊查询
存放路径:D:\git2\jwdev\articles\GORM\专家\安全与数据保护\敏感数据加密存储.md
📝 发现内容有误?点击此处直接编辑