Golang基于Vault實現(xiàn)敏感數(shù)據(jù)加解密
本文是《基于Vault的敏感信息保護》的姊妹篇,文中涉及的配置管理實現(xiàn)方案可以參考《淺談Golang配置管理》這篇文章。
背景
某些應(yīng)用程序會處理一些敏感的數(shù)據(jù),比如用戶的證件號碼、手機號等個人隱私數(shù)據(jù)。如果將這些敏感數(shù)據(jù)以明文形式存儲在數(shù)據(jù)庫中,一旦發(fā)生黑客入侵事件,這些數(shù)據(jù)很容易被竊取、泄露,從而引發(fā)用戶信任風(fēng)險和輿情危機,導(dǎo)致平臺用戶流失,甚至需要承擔(dān)法律責(zé)任。
數(shù)據(jù)加密是主要的數(shù)據(jù)安全防護技術(shù)之一,敏感數(shù)據(jù)應(yīng)該加密存儲在數(shù)據(jù)庫中,降低泄露風(fēng)險。
數(shù)據(jù)加解密方案
本文采用的是 HashiCorp 公司的 Vault 工具。Vault 通過自帶的 Transit 引擎提供加解密即服務(wù)(Encryption as a Service),如下圖所示,加解密過程為:
加密過程:
App 將需要加密的明文發(fā)給 Vault
Vault 將加密后的密文返給 App
App 將含有密文的數(shù)據(jù)存儲到數(shù)據(jù)庫中
解密過程:
App 從數(shù)據(jù)庫中讀取數(shù)據(jù)(含密文字段)
App 將需要解密的密文發(fā)給 Vault
Vault 將解密后的明文返給 App

具體實現(xiàn)過程
1. 準(zhǔn)備工作
使用 Vault 提供加解密服務(wù)前,需要先啟用 Transit 引擎,創(chuàng)建專用的加解密密鑰,并賦予對應(yīng)的 AppRole 加解密相關(guān)權(quán)限。
# 啟用 Transit 引擎
$ vault secrets enable transit
# 創(chuàng)建專用的加解密密鑰
$ vault write -f transit/keys/mykey
# 為 AppRole 綁定的權(quán)限策略 myapp-policy 添加加解密權(quán)限
$ vault policy write myapp-policy -<<EOF
#已有的權(quán)限,見《基于Vault的敏感信息保護》這篇文章
#新增加密權(quán)限:
path "transit/encrypt/mykey" {
???capabilities = [ "update" ]
}
#新增解密權(quán)限:
path "transit/decrypt/mykey" {
???capabilities = [ "update" ]
}
EOF
# 重新生成 AppRole 的 SecretID
$ vault write -f -field=secret_id auth/approle/role/myapp/secret-id >~/.secretid2. 初始化Vault客戶端
不同于《基于Vault的敏感信息保護》這篇文章,本文采用應(yīng)用程序與 Vault 直接集成的方案,使用的是 Vault 官方提供的 Go 語言庫。
在應(yīng)用程序與 Vault 交互前,需要初始化 Vault 客戶端:登錄 Vault 獲取 Token,并在 Token 過期前進行續(xù)租,當(dāng)無法續(xù)租時重新登錄獲取新的 Token。示例代碼如下:
func VaultInit() {
// 創(chuàng)建 Vault Client
config := vault.DefaultConfig()
config.Address = vaultAddress
var err error
VaultClient, err = vault.NewClient(config)
if err != nil {
log.Fatalf("Failed to create vault client, err: %v", err)
}
// 循環(huán):登錄認證,并續(xù)租Token
go func() {
for {
vaultLoginResp, err := login(VaultClient)
if err != nil {
log.Printf("Unable to authenticate to Vault: %v", err)
time.Sleep(time.Second * 10)
continue
}
tokenErr := renew(VaultClient, vaultLoginResp)
if tokenErr != nil {
log.Printf("Unable to start managing token lifecycle: %v", tokenErr)
time.Sleep(time.Second * 10)
}
}
}()
}本文采用的 Vault 相關(guān)配置如下:
vault:
address: http://x.x.x.x:8200
transit:
key: mykey
auth:
roleid-file-path: /app/role/roleid
secretid-file-path: /app/role/secretid3. 登錄認證
本文選擇 AppRole 認證方法,登錄 Vault 的示例代碼如下:
func login(client *vault.Client) (*vault.Secret, error) {
// 讀取 RoleID
bytes, err := ioutil.ReadFile(vaultRoleIdFilePath)
if err != nil {
return nil, fmt.Errorf("Error reading role ID file: %w", err)
}
roleID := strings.TrimSpace(string(bytes))
if len(roleID) == 0 {
return nil, errors.New("Error: role ID file exists but read empty value")
}
// 指定 SecretID
secretID := &auth.SecretID{FromFile: vaultSecretIdFilePath}
// 初始化 AppRole 認證方法,指定身份憑據(jù)
appRoleAuth, err := auth.NewAppRoleAuth(roleID, secretID)
if err != nil {
return nil, fmt.Errorf("unable to initialize AppRole auth method: %w", err)
}
// 通過 AppRole 認證方法登錄到 Vault
authInfo, err := client.Auth().Login(context.Background(), appRoleAuth)
if err != nil {
return nil, fmt.Errorf("unable to login to AppRole auth method: %w", err)
}
if authInfo == nil {
return nil, fmt.Errorf("no auth info was returned after login")
}
log.Printf("Successfully (re)logined, lease duration: %ds", authInfo.Auth.LeaseDuration)
return authInfo, nil
}4. Token續(xù)租
renew函數(shù)監(jiān)聽Token的生命周期,在TTL到期前進行續(xù)租操作,直到無法繼續(xù)續(xù)租、續(xù)租失敗為止,此時需要重新登錄,獲取新的 Token。renew函數(shù)的示例代碼如下:
func renew(client *vault.Client, token *vault.Secret) error {
// 為 Token 創(chuàng)建一個監(jiān)聽器
watcher, err := client.NewLifetimeWatcher(&vault.LifetimeWatcherInput{
Secret: token,
//Increment: 3600,
})
if err != nil {
return fmt.Errorf("unable to initialize new lifetime watcher for renewing auth token: %w", err)
}
// 啟動后臺續(xù)租協(xié)程
go watcher.Start()
defer watcher.Stop()
for {
select {
// 續(xù)租失敗,或者無法繼續(xù)續(xù)租
case err := <-watcher.DoneCh():
//續(xù)租失敗
if err != nil {
log.Printf("Failed to renew token: %v. Re-attempting login.", err)
return nil
}
// 無法繼續(xù)續(xù)租
log.Printf("Token can no longer be renewed. Re-attempting login.")
return nil
// 成功完成續(xù)租
case renewal := <-watcher.RenewCh():
log.Printf("Successfully renewed, lease duration: %ds", renewal.Secret.Auth.LeaseDuration)
}
}
}5. 加密
本文以 GORM 庫為例來說明。GORM 的 Hook 機制允許在數(shù)據(jù)庫 CRUD 操作前后執(zhí)行預(yù)定義的 Hook 方法。對于加密而言,可以為模型類定義 BeforeSave 方法,并在其中完成敏感數(shù)據(jù)的加密操作。
func (t *Teacher) BeforeSave(*gorm.DB) error {
return t.Encrypt()
}Teacher 模型包含證件號碼IDcard和手機號Phone兩個敏感數(shù)據(jù):
// 此處僅展示 GORM 相關(guān)標(biāo)簽,省略其它標(biāo)簽
type Teacher struct {
gorm.Model
Name string
// ... 其余字段省略
//密文
IDcard string `gorm:"unique"`
Phone string
//明文
PlainIDcard string `gorm:"-"`
PlainPhone string `gorm:"-"`
}加密方法Encrypt借助 Vault 對 IDcard 和 Phone 進行加密操作,示例代碼如下:
func (t *Teacher) Encrypt() error {
path := fmt.Sprintf("/transit/encrypt/%s", config.VaultTransitKey)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// 批量加密
resp, err := Vault.Logical().WriteWithContext(ctx, path, map[string]interface{}{
"batch_input": []map[string]interface{}{
{
"plaintext": base64.StdEncoding.EncodeToString([]byte(t.PlainIDcard)),
},
{
"plaintext": base64.StdEncoding.EncodeToString([]byte(t.PlainPhone)),
},
},
})
if err != nil {
log.Printf("teacher.Encrypt failed to encrypt data")
return err
}
// 拿到密文
t.IDcard = resp.Data["batch_results"].([]interface{})[0].(map[string]interface{})["ciphertext"].(string)
t.Phone = resp.Data["batch_results"].([]interface{})[1].(map[string]interface{})["ciphertext"].(string)
log.Printf("teacher.Encrypt called")
return nil
}6. 解密
解密的實現(xiàn)與加密類似,我們可以定義解密方法Decrypt,當(dāng)需要進行解密時調(diào)用該方法:
- 如果沒有使用緩存層,可以在
AfterFind方法中調(diào)用Decrypt,在查詢數(shù)據(jù)庫后完成解密操作 - 如果使用了 Redis 等緩存服務(wù),則需要在更新緩存或命中緩存之后調(diào)用
Decrypt
Decrypt方法的示例代碼如下。
func (t *Teacher) Decrypt() error {
path := fmt.Sprintf("/transit/decrypt/%s", config.VaultTransitKey)
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
// 批量解密
resp, err := Vault.Logical().WriteWithContext(ctx, path, map[string]interface{}{
"batch_input": []map[string]interface{}{
{
"ciphertext": t.IDcard,
},
{
"ciphertext": t.Phone,
},
},
})
if err != nil {
log.Printf("teacher.Decrypt failed to decrypt data")
return err
}
// 拿到 base64 文本
IDcard_base64 := resp.Data["batch_results"].([]interface{})[0].(map[string]interface{})["plaintext"].(string)
Phone_base64 := resp.Data["batch_results"].([]interface{})[1].(map[string]interface{})["plaintext"].(string)
// 解碼拿到明文
IDcard, err1 := base64.StdEncoding.DecodeString(IDcard_base64)
Phone, err2 := base64.StdEncoding.DecodeString(Phone_base64)
if err1 != nil || err2 != nil {
log.Printf("teacher.Decrypt failed to base64 decode")
return errors.New("base64 decode error")
}
t.PlainIDcard = string(IDcard)
t.PlainPhone = string(Phone)
log.Printf("teacher.Decrypt called")
return nil
}總結(jié)
數(shù)據(jù)加密是主要的數(shù)據(jù)安全防護技術(shù)之一,敏感數(shù)據(jù)應(yīng)該加密存儲在數(shù)據(jù)庫中,降低泄露風(fēng)險。本文介紹了 Golang 基于 Vault 實現(xiàn)敏感數(shù)據(jù)加解密的方案和具體實現(xiàn)過程。
到此這篇關(guān)于Golang基于Vault實現(xiàn)敏感數(shù)據(jù)加解密的文章就介紹到這了,更多相關(guān)Golang Vault敏感數(shù)據(jù)加解密內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
golang連接MongoDB數(shù)據(jù)庫及數(shù)據(jù)庫操作指南
MongoDB是Nosql中常用的一種數(shù)據(jù)庫,下面這篇文章主要給大家介紹了關(guān)于golang連接MongoDB數(shù)據(jù)庫及數(shù)據(jù)庫操作的相關(guān)資料,文中通過實例代碼介紹的非常詳細,需要的朋友可以參考下2022-09-09
Go 語言中接口類型轉(zhuǎn)換為具體類型的三種實現(xiàn)
本文主要介紹了Go 語言中接口類型轉(zhuǎn)換為具體類型的實現(xiàn),主要有類型斷言、類型選擇和反射三種方式,具有一定的參考價值,感興趣的可以了解一下2025-06-06
Go 語言遞歸函數(shù)實現(xiàn)方法及應(yīng)用場景
遞歸函數(shù)是編程中一種重要的概念,尤其在Go語言中,遞歸被廣泛應(yīng)用于算法設(shè)計和程序開發(fā)中,本文將詳細介紹Go語言中的遞歸函數(shù),包括其基本概念、實現(xiàn)方法以及應(yīng)用場景,感興趣的朋友跟隨小編一起看看吧2025-11-11

