aes-gcm 加密前必须生成16/24/32字节固定长度密钥,推荐用sha256哈希后取32字节;密钥须从环境变量或kms获取;每次加密需生成12字节随机nonce并拼接密文存储;数据库字段需预留足够空间(明文长度+28字节);解密失败应返回泛化错误。

用 aes.GCM 加密敏感字段前必须先生成固定长度密钥
Go 标准库不提供开箱即用的“加密字符串”函数,aes.GCM 是最常用也最稳妥的选择,但它要求密钥长度严格为 16、24 或 32 字节。直接用字符串当密钥会 panic:crypto/aes: invalid key size 12。
实操建议:
- 用
sha256.Sum256对原始密钥字符串哈希,再取前 32 字节作为 AES-256 密钥 - 密钥不能硬编码在代码里,应从环境变量或 KMS 读取(比如
os.Getenv("ENCRYPTION_KEY")) - 如果业务允许,直接用
crypto/rand.Read生成一次性的随机密钥并安全存储(如 Vault),比字符串派生更可靠
加密时必须为每个字段生成独立 nonce,复用会导致密文可被重放或破解
nonce 不是随便填的随机数,它是 GCM 模式下保证相同明文产生不同密文的关键。同一个 nonce + 同一把密钥加密两次,攻击者能直接推断出明文异同——这在用户手机号、身份证号等低熵字段上尤其危险。
实操建议:
- 每次加密前调用
crypto/rand.Read(nonce[:])生成 12 字节随机nonce(GCM 推荐长度) - 把
nonce和密文拼在一起存入数据库,例如append(nonce[:], ciphertext...),解密时前 12 字节切出来即可 - 绝不要用时间戳、自增 ID 或用户 ID 当
nonce——它们不具备密码学随机性
数据库字段类型要预留足够空间,密文比原文长且不可压缩
AES-GCM 加密后,输出长度 = 明文长度 + 12(nonce)+ 16(auth tag)。一个 11 位手机号(UTF-8 下 11 字节)加密后变成 39 字节;若原文含中文,按 UTF-8 编码计算字节数,不是 rune 数。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
常见错误现象:
- MySQL
VARCHAR(20)存加密后的邮箱 → 插入失败或被截断 → 解密报cipher: message authentication failed - PostgreSQL 使用
TEXT虽然安全,但没做长度校验,导致前端传超长明文时后端静默加密,数据库写入成功但解密失败
实操建议:
- MySQL 建表时敏感字段设为
VARCHAR(255)或TEXT,别抠字节 - 在 ORM 层(如 GORM)写入前加校验:
if len(plaintext) > 100 { return errors.New("plaintext too long") },防异常输入放大密文体积
解密失败时别直接暴露 cipher: message authentication failed
这个错误实际意味着:密文被篡改、nonce 错了、密钥错了,或者数据损坏。但如果直接返回给调用方,等于告诉攻击者“你改的这部分确实影响了解密逻辑”,属于侧信道泄漏。
实操建议:
- 统一转成泛化错误:
return nil, errors.New("failed to decrypt field"),不带任何 crypto 库内部信息 - 记录完整上下文到日志(含 traceID、字段名、密文 hex 前 8 字节),但日志不进标准输出,避免被容器日志系统意外暴露
- 对空密文、全零密文、长度小于 28 字节的输入,提前返回错误,避免进入 GCM 解密流程暴露行为差异
密钥管理、nonce 随机性、密文长度控制、错误掩盖——这四点漏掉任一环,加密就形同虚设。尤其是开发环境常把密钥写死、测试数据用固定 nonce,上线后几乎必然出问题。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










