动态脱敏用于不可逆场景(如日志、展示),加密用于需还原场景(如落盘、传输);脱敏不改原始值,加密必须可逆且带完整性校验;禁用base64替代加密、禁用bcrypt存手机号、禁用AES再加密password_hash。

Go 里对敏感字符串做“动态脱敏 + 加密存储”,本质是两个独立动作:脱敏用于展示/日志等**不可逆场景**,加密用于落盘/传输等**需还原场景**。混用或错位(比如拿 bcrypt 存手机号、拿 AES 加密日志)会导致数据不可用或安全失效。
什么时候该脱敏,什么时候该加密
脱敏(如 138****1234)只在输出端发生,不改变原始值,也不需要解密逻辑;加密(如 AES-GCM 密文)必须可逆,且要带完整性校验。常见误用:
- 把
password_hash再 AES 加密 —— 哈希本身已不可逆,再加密纯属冗余,还破坏验证流程 - 用
base64.StdEncoding.EncodeToString存数据库密码字段 —— 这不是加密,是编码,内存一 dump 就还原 - 在 HTTP 响应 JSON 里靠中间件正则替换
"password":"xxx"—— 漏嵌套、破转义、误杀键名
动态脱敏:用 json.Marshaler 控制输出
结构体字段值不变,只在 json.Marshal 时按规则替换。必须用指针接收器,否则无法处理 nil 字段或嵌套结构:
type User struct {
Name string `json:"name"`
Phone string `json:"phone"`
IDCard string `json:"id_card"`
}
func (u *User) MarshalJSON() ([]byte, error) {
type Alias User // 防止无限递归
aux := &struct {
Phone string `json:"phone"`
IDCard string `json:"id_card"`
*Alias
}{
Phone: redactPhone(u.Phone),
IDCard: redactIDCard(u.IDCard),
Alias: (*Alias)(u),
}
return json.Marshal(aux)
}
func redactPhone(s string) string {
if len(s) != 11 {
return "[invalid]"
}
return s[:3] + "****" + s[7:]
}
注意:别在 MarshalJSON 里调 fmt.Sprintf 拼 JSON 字符串,引号和转义极易出错;用内嵌 struct + json.Marshal 最稳妥。
立即学习“go语言免费学习笔记(深入)”;
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
AES-GCM 加密存储:密钥、nonce、拼接三处不能错
加密单个字符串字段(如数据库 api_token)时,cipher.NewGCM 是唯一推荐路径。关键点:
- 密钥必须是 32 字节:用
sha256.Sum256([]byte(os.Getenv("ENCRYPTION_KEY"))).[:]派生,别直接[]byte("mykey") - nonce 必须每次加密新生成且长度为 12 字节:
var nonce [12]byte; rand.Read(nonce[:]) - 密文必须拼接
nonce + ciphertext + tag存储:解密时前 12 字节切出来当 nonce,剩余部分传给aesgcm.Open - 解密失败必须显式检查
err != nil:cipher.ErrAuthentication不 panic,但返回的切片可能是乱码
示例加密函数:
func encryptString(plaintext string, block cipher.Block) ([]byte, error) {
aesgcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
nonce := make([]byte, aesgcm.NonceSize())
if _, err := rand.Read(nonce); err != nil {
return nil, err
}
ciphertext := aesgcm.Seal(nonce, nonce, []byte(plaintext), nil)
return ciphertext, nil
}
字段级加解密如何接入 Viper 或 GORM
整配置文件或整 struct 加密会破坏工具链(viper 无法 UnmarshalKey,GORM 无法 Scan)。真实做法是只处理具体字段:
- Viper:先
viper.ReadInConfig(),再viper.AllSettings()遍历键路径,匹配["database.password", "redis.auth"]等白名单,对对应字符串值调decryptString - GORM:实现
driver.Valuer和sql.Scanner接口,在Value()中加密、Scan()中解密,字段类型设为[]byte或string,避免用TEXT存 base64 - 密钥绝不能硬编码:从环境变量读取时记得
strings.TrimSpace,换行符会导致cipher.ErrAuthentication且无提示
最易被忽略的是:Viper 的 viper.Set("a.b.c", val) 不支持嵌套路径,得先 viper.Get("a.b") 取出 map,再手动赋值;GORM 的 Valuer 实现中,若字段是 *string,解密后要确保指针非 nil。

















