不能直接用 aes-gcm 做多租户字段加密,因其 nonce 必须唯一且 gcm 的完整性校验会在租户密钥混用时直接 panic,难以区分越权与数据损坏;应改用 aes-cbc + hmac-sha256,每个租户独立密钥、iv 随机生成并拼接存储,通过 tenantcrypto 结构体绑定租户上下文,敏感字段加密存储同时为等值查询增设哈希列,严格覆盖密钥为空、mac 失败、非 ascii 租户 id 等边界 case。

为什么不能直接用 AES-GCM 做多租户字段加密
因为 AES-GCM 的 nonce 必须唯一,而多租户场景下若共用密钥+简单递增 nonce,不同租户的相同字段(如手机号)加密后可能产生可预测的密文模式,导致租户间侧信道泄露。更关键的是,GCM 自带完整性校验,一旦租户密钥混用或解密时传错租户 ID,会直接 panic 报 cipher: message authentication failed,而不是返回空或错误码——这会让业务层难以区分是数据损坏还是租户越权。
实操建议:
- 每个租户必须分配独立密钥,且密钥不落地存储在数据库中,而是由 KMS(如 HashiCorp Vault)按租户 ID 动态获取
- 放弃
AES-GCM,改用AES-CBC+ HMAC-SHA256 组合(即 Encrypt-then-MAC),把租户 ID 作为 MAC key 的 salt 输入,确保同一明文在不同租户下生成完全不同密文和 MAC - 禁止在加密函数中硬编码 IV;IV 必须随机生成并和密文拼接存储(如
base64(IV || ciphertext)),解密时先切分再验证
如何让 encrypt/decrypt 函数自动绑定租户上下文
Go 的 context.Context 本身不携带租户信息,强行塞 context.WithValue(ctx, tenantKey, "t123") 容易被中间件覆盖或遗漏。更可靠的做法是在 DAO 层封装一个带租户感知的加解密器实例。
实操建议:
- 定义结构体
TenantCrypto,内部持有一个tenantID string和从 KMS 获取的encryptKey, macKey []byte - 方法
Encrypt(field string) (string, error)中:生成 16 字节随机 IV → AES-CBC 加密 → 计算 HMAC(IV || ciphertext) → 返回base64(IV + ciphertext + mac) - 方法
Decrypt(encrypted string) (string, error)中:base64 解码 → 切出前 16 字节为 IV,倒数 32 字节为 MAC,中间为密文 → 验证 HMAC → CBC 解密 → UTF-8 校验 - 避免在 handler 层调用
Encrypt;统一收口到 ORM 的BeforeSave和AfterFind钩子中,钩子内通过db.Statement.Context提取租户 ID(前提是 GORM 已注入)
数据库字段怎么存才不会破坏索引和查询语义
敏感字段加密后变成随机字节串,直接存 TEXT 类型会导致无法做 = 查询、LIKE 模糊匹配失效、ORDER BY 失效。但又不能为了可搜索牺牲安全性。
实操建议:
- 对需要等值查询的字段(如身份证号、邮箱),额外增加一个
field_hash列,存HMAC-SHA256(plaintext, tenantID + pepper)的 hex 值,查询时用 hash 匹配,原字段仍加密存储 - 对需要范围查询的字段(如出生日期),不加密,改用「脱敏+掩码」:存 YYYY-MM-01 或 YYYY-01-01,精度降级,业务层自行处理逻辑
- 禁止给加密字段建普通索引;如果必须加速检索,用 PostgreSQL 的
pgcrypto扩展配合digest()函数在 DB 层做哈希,但需确保应用层与 DB 层的哈希逻辑完全一致(包括 salt 和算法)
测试时最容易漏掉的三个边界 case
本地跑通不代表生产安全。这三个点不覆盖,上线后会出现租户数据错乱或静默解密失败:
- 租户密钥为空时,
Encrypt应 panic 或返回明确错误(如tenant key not found for t456),而不是 fallback 到默认密钥 —— 后者会让所有租户共享同一密钥 - 解密时遇到损坏密文(如 base64 截断、MAC 校验失败),必须返回
ErrDecryptionFailed而不是nil,否则上层代码可能把空字符串当成合法值写入业务逻辑 - 当租户 ID 含非 ASCII 字符(如中文、emoji)时,KMS 获取密钥前必须先做
sha256.Sum256([]byte(tenantID)).String()作为 key name,否则不同系统对 UTF-8 编码处理不一致,导致密钥拉取失败
租户隔离不是加个加密函数就完事;密钥生命周期、IV 管理、错误传播路径、DB 查询语义,每一环断掉都会让加密形同虚设。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











