aes-gcm是go微服务中唯一可落地的敏感数据加密方案,因base64仅为可逆编码、md5/sha256为单向摘要,均不满足可逆加密与完整性校验要求;密钥须严格为16/24/32字节,nonce每次加密必须全新随机生成12字节且不可复用,错误处理须显式校验err避免静默失败。

Go 微服务里对敏感数据做 AES 加密存储,cipher.NewGCM 是唯一可落地的方案;其他方式要么不安全(base64)、要么不可逆(md5/sha256)、要么极易出错(手搓 CBC+HMAC)。
为什么不能用 base64 或 md5 当“加密”
base64 是编码不是加密,base64.StdEncoding.EncodeToString([]byte("password123")) 输出完全可逆,日志一打、内存一 dump 就暴露;md5 和 sha256 是单向摘要,根本没法还原,不满足“存进去、读出来用”的基本需求。你真正要的是可逆 + 带完整性校验的对称加密——只有 AES-GCM 同时满足机密性与认证性,且 Go 标准库原生支持。
crypto/aes+cipher.NewGCM 三处致命坑
密钥长度、nonce 生成、错误处理这三点踩错一个,解密就失败,而且只返回 cipher.ErrAuthentication 或空切片,几乎无法定位原因。
- 密钥必须是 32 字节(
AES-256),不能直接用字符串字面量如[]byte("my-key")(长度不对)。生产环境应从环境变量读原始字符串,再用sha256.Sum256哈希后取前 32 字节,或更安全地用pbkdf2.Key派生(加随机 salt、≥100000 轮迭代) -
nonce必须每次加密都新生成,调用crypto/rand.Read(nonce[:]),长度严格为aesgcm.NonceSize()(通常是 12 字节)。绝不能用时间戳、自增 ID、用户 ID 等可预测值,否则 GCM 安全性归零 -
aesgcm.Open认证失败时不会 panic,而是返回cipher.ErrAuthentication或cipher.ErrInvalidLength;若忽略 error 直接用返回切片,可能拿到乱码甚至部分解密内容——这比报错更危险,必须显式检查err == nil
字段级加密比整结构加密更实用
加密整个配置文件或整个 struct 会导致工具链断裂:viper 无法 UnmarshalKey,jq 解析失败,git diff 失去可读性。真实项目中应只加密具体字段值,例如 database.password 或 api.token:
- 用
viper.AllSettings()获取顶层map[string]interface{},递归遍历键路径,匹配预设敏感键列表(如["database.password", "redis.auth"]) - 对匹配到的字符串值调用
aesgcm.Open解密,成功后直接覆写内存中的值 - 加密时也只操作原始明文字符串,
base64编码后写回 YAML/TOML 对应字段,其余结构完全不动
文件流加密必须确认 io.Seeker 支持
AES-GCM 加密前必须确认文件流支持 io.Seeker,因其需预写认证标签(tag)且无法回溯不可 seek 流(如 HTTP Body)。常见报错表面是 crypto/aes: invalid key size,其实是底层调用失败后错误掩盖,或更隐蔽的 write failed: bad message authentication code。
- 对不可 seek 的流(如管道、HTTP body),优先选
crypto/cipher.Stream模式(如 CFB、OFB),它们可边读边加密,无需 tag 回写 - 若必须用 GCM,先将流完整读入内存或临时文件,确保能 seek 到开头再加密;或改用
io.Pipe+goroutine 预读缓冲,但需自行管理nonce和 tag 位置 - 检查流类型:
_, ok := reader.(io.Seeker),不 ok 就别硬套cipher.NewGCM的标准封装
最易被忽略的点:nonce 复用是 GCM 最致命漏洞——相同 key + 相同 nonce 会导致密钥部分泄露,攻击者可能直接恢复明文;Go 标准库不会帮你校验是否重复,它只信你给的值。











