一、前言
Hi,开门见山的说,这次给大家带来的是关于 Gorm Hook 机制的落地场景,笔者也是在Gorm官方文档中了解到有Hook机制的存在,不过一直没有找到过太多合适的场景来使用。
最近刚好在做一块新业务的设计,因为涉及到金融相关属性,对于非业务逻辑层面的数据库篡改可能会导致的资损问题进行了重新分析,之前的签名操作是直接hard code编码在代码中的,存在诸多问题:
- 手动挨个方法添加签名和验签处理,容易遗漏导致故障
- 秘钥管理粗暴,配置文件各种传递,没有集中进行管理
- 密钥安全性存在风险,研发人员可以轻易打印获取密钥
所以就有了今天的文章内容
- Gorm Hook官方文档:https://gorm.io/docs/hooks.html
- 内存密钥保护库:https://github.com/awnumar/memguard
二、Gorm Hook 机制设计
在开始动代码之前先要解决的问题和要达到的目标:
- 无侵入 对于正常业务开发无需关心其中逻辑
- 密钥无法再程序中直接获取明文
- 可支持扩展签名KYC密文初始化,全过程不暴露密钥
- 可扩展对内存密钥进行保护
在过程中不止是需要使用Gorm Hook的能力,还需要对于密钥管理进行一定的约束
在实现过程中首先考虑的是密钥的初始化,后续再Hook中按照表的维度来使用不同的密钥来参与加验签过程:
这里实现的是基于Hmac256的密钥管理模块,可以其中引入 “memguard” 来加固内存,和对key进行解密操作等;
var keys sync.Map
var keyInit sync.Once
// SetTableSignHmac256Key 设置Hmac256密钥
func SetTableSignHmac256Key(table schema.Tabler, key string) error {
keyInit.Do(func() {
keys = sync.Map{}
})
_, ok := keys.Load(table.TableName())
if ok {
return errors.New("the table sign key cannot be set repeatedly")
}
keys.Store(table.TableName(), key)
return nil
}
// SignDataHmac256 数据签名
func SignDataHmac256(table schema.Tabler, values ...string) (string, error) {
signValue := ""
for _, v := range values {
signValue += v
}
if signValue == "" {
return "", errors.New("no valid value")
}
key, ok := keys.Load(table.TableName())
if !ok {
return "", errors.New("sign error no available key exists")
}
return codec.HmacBase64([]byte(cast.ToString(key)), signValue), nil
}
// CheckSignDataHmac256 校验密钥
func CheckSignDataHmac256(table schema.Tabler, sign string, values ...string) (bool, error) {
hmac256, err := SignDataHmac256(table, values...)
if err != nil {
return false, err
}
return hmac256 == sign, nil
}
三、代码实现
当把密钥做好管理之后,后面就比较简单了,在逻辑代码中增加 Gorm Hook 方法即可
初始化密钥:
// 在项目初始化的时候初始化密钥
err := ugorm.SetTableSignHmac256Key(&model.User{},Key)
if err != nil {
logx.Must(errors.New("not config CustodyWalletRecordsSignKey"))
}
在具体测model方法中增加 Hook,此时在对数据库做读写操作之前都会对密钥进行加签验签 以达到最终的目的。
type User struct {
ID int64 `json:"id" gorm:"column:id;primary_key;type:bigint AUTO_INCREMENT"`
UserName string `json:"user_name" gorm:"column:user_name;type:varchar(255) NOT NULL;default:'';comment:用户名;"`
Email string `json:"email" gorm:"column:email;type:varchar(255) NOT NULL;default:'';comment:邮箱;index:email_phone"`
CountryShort string `json:"country_short" gorm:"column:country_short;type:varchar(64) NOT NULL;default:'';comment:手机区号;index"`
Phone string `json:"phone" gorm:"column:phone;type:varchar(64) NOT NULL;default:'';comment:手机号;index"`
Password string `json:"password" gorm:"column:password;type:varchar(64) NOT NULL;default:'';comment:密码;"`
Language string `json:"language" gorm:"column:language;type:varchar(64) NOT NULL;default:'';comment:语言;"`
Status int64 `json:"status" gorm:"column:status;type:tinyint(4) NOT NULL;default:0 ;comment:1:启用,2停用;"`
Sign string `gorm:"column:sign;type:varchar(255);not null" json:"sign"`
UpdatedAt time.Time `json:"updated_at" gorm:"column:updated_at;NOT NULL;default:CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;type:TIMESTAMP"`
CreatedAt time.Time `json:"created_at" gorm:"column:created_at;NOT NULL;default:CURRENT_TIMESTAMP;type:TIMESTAMP;index"`
DeletedAt *time.Time `json:"deleted_at" gorm:"column:deleted_at;type:DATETIME"`
}
// --------------------------- ↓↓↓ 签名 hook 方法 ↓↓↓ ---------------------------
func (c *User) toSignString() string {
return c.UserName + c.Email + c.Phone + c.Password
}
// BeforeCreate 数据创建时进行签名
func (c *User) BeforeCreate(tx *gorm.DB) (err error) {
hmac256, err := ugorm.SignDataHmac256(c, c.toSignString())
if err != nil {
return err
}
c.Sign = hmac256
return
}
// BeforeUpdate 数据更新时进行验签
func (c *User) BeforeUpdate(tx *gorm.DB) (err error) {
hmac256, err := ugorm.SignDataHmac256(c, c.toSignString())
if err != nil {
return err
}
c.Sign = hmac256
return
}
// AfterFind 数据查询时进行验签
func (c *User) AfterFind(tx *gorm.DB) (err error) {
ok, err := ugorm.CheckSignDataHmac256(c, c.Sign, c.toSignString())
if err != nil {
return err
}
if !ok {
return errors.New(cast.ToString(c.ID) + ": signature verification failure")
}
return
}
// --------------------------- ↑↑↑ 签名 hook 方法 ↑↑↑ ---------------------------