第一章:Go加盐去盐不是功能需求,是合规红线:GDPR/等保2.0/PCI-DSS三级认证强制要求详解
在数据安全合规语境下,“加盐哈希”绝非可选的工程优化手段,而是多项国际与国内强制性标准的刚性技术基线。GDPR第32条明确要求“采用伪匿名化等适当技术措施保护个人数据”,等保2.0《基本要求》第三级中“身份鉴别”控制项(a)强制规定:“应对登录用户的口令等关键信息进行不可逆加密存储”,而PCI-DSS v4.0第8.2.1条直接指出:“不得以明文形式存储口令、PIN或CVV;所有口令必须使用强哈希算法(如bcrypt、scrypt、Argon2)并配合唯一盐值处理”。
为什么标准拒绝简单SHA-256?
- 明文口令易被彩虹表批量破解
- 固定盐值使相同口令生成相同哈希,暴露用户行为模式
- 等保2.0测评中若发现盐值硬编码或全局复用,直接判定“身份鉴别控制项不满足”
Go语言合规实现核心步骤
使用golang.org/x/crypto/bcrypt是PCI-DSS与等保2.0共同推荐方案(无需自行实现盐值管理):
package main
import (
"fmt"
"golang.org/x/crypto/bcrypt"
)
func hashPassword(password string) (string, error) {
// bcrypt.GenerateFromPassword自动创建随机盐值(12轮成本因子)
// 盐值与哈希密文一同编码进返回字符串($2a$12$...格式),无需单独存储
hashed, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("failed to hash password: %w", err)
}
return string(hashed), nil
}
func verifyPassword(hashedPassword, inputPassword string) bool {
// bcrypt.CompareHashAndPassword自动解析盐值并重执行哈希比对
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(inputPassword))
return err == nil
}
合规要点对照表
| 标准 | 关键条款 | Go实现对应要求 |
|---|---|---|
| GDPR | 第32条 | 盐值必须唯一、不可预测(bcrypt内置保障) |
| 等保2.0三级 | 8.1.2.a | 禁止使用MD5/SHA-1;必须使用bcrypt/scrypt等自适应哈希 |
| PCI-DSS v4.0 | 8.2.1 & 8.2.3 | 盐值不得复用;哈希输出须含完整盐值与参数信息 |
任何绕过bcrypt、改用sha256.Sum256手动拼接盐值的方案,均无法通过等保2.0现场测评——因缺乏成本因子调节能力,无法抵御暴力穷举。
第二章:加盐密码学原理与Go标准库实践
2.1 密码哈希的数学基础与抗碰撞设计
密码哈希的本质是构造一个确定性、单向、抗碰撞性强的压缩映射:$ H: {0,1}^* \to {0,1}^n $。其安全性根植于离散对数、大整数分解或布尔函数雪崩效应等数学难题。
为什么MD5已不安全?
- 碰撞可在秒级构造(如王小云2005年攻击)
- 输出长度仅128位,生日攻击复杂度仅 $2^{64}$
现代哈希的设计支柱
- 混淆(Confusion):输入微小变化导致输出全局雪崩
- 扩散(Diffusion):每位输入影响尽可能多的输出位
- 抗长度扩展:采用HMAC或SHA-3的海绵结构
import hashlib
# 推荐:SHA-256 + salt(防止彩虹表)
password = b"my_secret_123"
salt = b"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
hashed = hashlib.pbkdf2_hmac('sha256', password, salt, 600000, dklen=32)
print(hashed.hex()[:32]) # 输出:e8e2a...(64字符hex)
此代码使用PBKDF2-HMAC-SHA256:
600000次迭代增强抗暴力能力;dklen=32指定32字节密钥长度;盐值本身是SHA-256哈希,确保唯一性与不可预测性。
| 属性 | SHA-256 | SHA-3 (Keccak) | BLAKE3 |
|---|---|---|---|
| 输出长度 | 256 bit | 可变(224–512) | 256 bit(默认) |
| 抗长度扩展 | 否(需HMAC) | 是 | 是 |
| 吞吐性能 | 中等 | 较低 | 极高 |
graph TD
A[明文密码] --> B[加盐]
B --> C[多轮迭代哈希]
C --> D[密钥派生]
D --> E[存储哈希值]
2.2 Go crypto/rand 与 crypto/hmac 的安全随机数生成实战
为什么 math/rand 不适用于安全场景
- 生成可预测序列,无熵源依赖
- 缺乏密码学强度,易被逆向或爆破
- 仅适合模拟、测试等非敏感用途
安全随机数生成核心流程
func secureToken() ([]byte, error) {
b := make([]byte, 32)
if _, err := rand.Read(b); err != nil { // 从操作系统熵池(/dev/urandom 或 CryptGenRandom)读取
return nil, err
}
return b, nil
}
rand.Read() 直接调用底层安全随机源;参数 b 必须为预分配的切片,函数填充其全部字节并返回实际写入长度(恒等于 len(b))和可能错误。
HMAC 增强随机性:密钥派生示例
| 步骤 | 说明 |
|---|---|
| 输入 | 安全随机种子 + 高熵盐值 + 上下文标签 |
| 算法 | hmac.New(sha256.New, key) |
| 输出 | 固定长度伪随机字节,抗碰撞且不可逆 |
graph TD
A[OS Entropy Pool] --> B[crypto/rand.Read]
B --> C[原始随机字节]
C --> D[hmac.Sum256 with secret key]
D --> E[密码学安全派生密钥]
2.3 bcrypt vs scrypt vs Argon2:Go生态选型对比与性能压测
密码哈希算法的选择直接影响系统抗暴力破解能力。Go 生态中主流实现如下:
golang.org/x/crypto/bcrypt:基于 Blowfish,仅支持 CPU 阻力(cost 参数)github.com/elithrar/simple-scrypt:轻量 scrypt 封装,需手动配置 N, r, pgithub.com/go-tk/argon2:符合 RFC 9106 的 Argon2id 实现,支持内存、CPU、并行度三重可调
性能压测关键指标(1MB 内存限制,1s 时间窗口)
| 算法 | 吞吐量(ops/s) | 内存峰值 | 抗 GPU 能力 |
|---|---|---|---|
| bcrypt | ~1,200 | ~4 KB | 弱 |
| scrypt | ~380 | ~1 MB | 中 |
| Argon2 | ~290 | ~1 MB | 强 |
// Argon2id 推荐参数(Go 实现)
hash, _ := argon2.IDKey([]byte("pwd"), salt, 3, 64*1024, 4, 32) // time=3, memory=64MB, threads=4
time=3 表示迭代次数(影响CPU成本),64*1024 是 KiB 单位内存用量(64MB),threads=4 启用并行计算,32 为输出密钥长度。Argon2 在同等资源下提供更均衡的抗侧信道与抗硬件加速能力。
2.4 基于golang.org/x/crypto/pbkdf2的合规盐值派生实现
PBKDF2 是 NIST SP 800-132 推荐的密钥派生函数,适用于密码哈希与密钥增强场景。其安全性高度依赖盐值(salt)的唯一性与随机性。
盐值生成规范
- 必须使用
crypto/rand.Reader(不可用math/rand) - 最小长度 ≥ 128 位(16 字节),推荐 32 字节
- 每次派生独立生成,禁止复用
核心实现示例
import (
"crypto/rand"
"golang.org/x/crypto/pbkdf2"
"hash"
"crypto/sha256"
)
func deriveKey(password, salt []byte) []byte {
return pbkdf2.Key(
password, // 原始口令(UTF-8 编码)
salt, // 随机盐值(32 字节)
1_000_000, // 迭代次数(≥10⁶,符合 OWASP 2023 建议)
32, // 输出密钥长度(字节)
sha256.New, // 伪随机函数(HMAC-SHA256)
)
}
逻辑分析:
pbkdf2.Key执行 HMAC-SHA256 基础的 PBKDF2-HMAC 运算;1_000_000次迭代显著增加暴力破解成本;32字节输出适配 AES-256 或密钥加密场景;盐值必须与派生结果持久化共存(如salt|derived_key存储)。
合规参数对照表
| 参数 | 推荐值 | 合规依据 |
|---|---|---|
| 迭代次数 | ≥ 1,000,000 | NIST SP 800-132 §5.1 |
| 盐长 | 32 字节 | RFC 8018 §5.1 |
| PRF | HMAC-SHA256 | FIPS 180-4 + SP 800-107 |
graph TD
A[原始口令] --> B[32B 随机盐]
B --> C[PBKDF2-HMAC-SHA256]
C --> D[1M 迭代]
D --> E[32B 派生密钥]
2.5 盐值存储策略:嵌入式盐 vs 独立元数据表的Go ORM适配方案
在密码哈希实践中,盐值(salt)的存储方式直接影响安全性与ORM可维护性。
嵌入式盐:字段内联设计
将 salt 作为 User 结构体字段直接持久化,简洁但耦合度高:
type User struct {
ID uint `gorm:"primaryKey"`
Email string `gorm:"uniqueIndex"`
Hash string `gorm:"size:255"`
Salt string `gorm:"size:32"` // Base64-encoded 24-byte salt
}
Salt字段与业务实体强绑定,GORM 自动映射;但迁移时难以统一轮换策略,且审计粒度粗。
独立元数据表:解耦与扩展性
引入 user_secrets 表分离敏感元数据:
| Column | Type | Description |
|---|---|---|
| user_id | BIGINT | 外键,引用 users.id |
| salt | BYTEA | 二进制盐值(24字节) |
| updated_at | TIMESTAMPTZ | 最后盐值更新时间 |
graph TD
A[User] -->|1:1| B[user_secrets]
B --> C[定期盐值轮换钩子]
解耦后支持独立加密策略、审计日志及多算法共存,GORM 通过
Preload或Join按需加载。
第三章:三大合规框架对加盐机制的硬性约束解析
3.1 GDPR第32条“适当技术措施”在密码存储中的Go代码映射
GDPR第32条要求采取“加密、伪匿名化等适当技术与组织措施”,密码存储必须抵御离线暴力破解与彩虹表攻击。
推荐哈希策略组合
- 使用
argon2id(v1.3+)替代过时的 bcrypt/scrypt - 强制盐值唯一性(32字节随机)与可调参数
- 禁用明文、MD5、SHA-1、无盐 SHA-256
Go 实现示例(基于 golang.org/x/crypto/argon2)
func HashPassword(password string) (string, error) {
salt := make([]byte, 32)
if _, err := rand.Read(salt); err != nil {
return "", err // GDPR要求盐值不可预测
}
hash := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32) // time=1, mem=64MB, threads=4, keyLen=32
return fmt.Sprintf("$argon2id$v=19$m=%d,t=%d,p=%d$%s$%s",
64*1024, 1, 4,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(hash),
), nil
}
逻辑分析:argon2.IDKey 执行抗侧信道的内存硬哈希;m=64*1024 满足GDPR“持续演进防御”要求;t=1 平衡延迟与安全性;输出格式兼容 PHC 标准,便于审计追踪。
| 参数 | 合规意义 | 典型值 |
|---|---|---|
m(内存) |
抵御ASIC/FPGA爆破 | ≥64 MiB |
t(时间) |
增加单次尝试成本 | ≥1 |
p(并行度) |
防范多核暴力 | ≥2 |
graph TD
A[用户密码] --> B[生成32B CSPRNG盐]
B --> C[Argon2id: m=64MiB, t=1, p=4]
C --> D[Base64编码输出]
D --> E[持久化存储:盐+哈希+参数]
3.2 等保2.0三级要求中“身份鉴别”条款的Go服务端验证落地
等保2.0三级明确要求:双因子认证、口令复杂度、登录失败处理、会话超时与令牌唯一性。Go服务端需在鉴权链路中精准落地。
核心验证逻辑分层
- ✅ 强制双因子(短信/OTP + 密码)
- ✅ 口令策略:长度≥8,含大小写字母+数字+特殊字符
- ✅ 连续5次失败后锁定账户30分钟
登录凭证校验代码片段
// validateLoginRequest 遵循GB/T 22239-2019第8.1.2.1条
func validateLoginRequest(req LoginReq) error {
if !regexp.MustCompile(`^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[\W_]).{8,}$`).MatchString(req.Password) {
return errors.New("密码不符合等保三级复杂度要求")
}
if time.Since(req.Timestamp) > 5*time.Minute { // 防重放
return errors.New("请求已过期(>5min),拒绝处理")
}
return nil
}
req.Timestamp 由客户端生成并签名,服务端校验时间漂移≤5分钟,满足等保对“防重放”的时效性约束。
登录失败策略映射表
| 触发条件 | 响应动作 | 合规依据 |
|---|---|---|
| 单IP 5次失败/小时 | 返回429,返回Retry-After | 等保2.0 8.1.2.3.b |
| 同账号3次失败 | 冻结账户30分钟(Redis TTL) | 8.1.2.3.c |
graph TD
A[接收登录请求] --> B{密码格式校验}
B -->|不通过| C[返回400+错误码]
B -->|通过| D[查询Redis账户冻结状态]
D -->|已冻结| E[返回403]
D -->|正常| F[执行OTP+密码双因子验证]
3.3 PCI-DSS v4.0 8.2.x条款对盐长度、唯一性、生命周期的Go运行时校验
PCI-DSS v4.0 第8.2.x条明确要求:盐值(salt)必须密码学随机生成、全局唯一、长度≥128位(16字节),且不得复用或持久化超过单次认证生命周期。
盐生成与即时校验逻辑
func generateAndValidateSalt() ([]byte, error) {
salt := make([]byte, 16) // ✅ 满足最小128位长度要求
if _, err := rand.Read(salt); err != nil {
return nil, fmt.Errorf("failed to generate cryptographically secure salt: %w", err)
}
// 运行时唯一性断言(如集成Redis原子setnx)
if !isSaltUniqueInContext(salt) {
return nil, errors.New("salt uniqueness violation — rejected by PCI-DSS 8.2.1")
}
return salt, nil
}
rand.Read() 调用系统级 CSPRNG(/dev/urandom 或 CryptGenRandom),确保熵源合规;isSaltUniqueInContext() 需在会话级上下文(如 Redis + TTL=30s)中执行原子去重,强制实现“一次一盐”。
合规性关键参数对照表
| 要求项 | PCI-DSS 8.2.1 | Go实现验证点 |
|---|---|---|
| 最小长度 | ≥128 bit | len(salt) == 16 |
| 唯一性保障 | 全局单次有效 | SETNX salt:hex 1 EX 30 |
| 生命周期上限 | ≤单次认证会话 | Redis TTL ≤ auth session TTL |
校验流程(运行时强制拦截)
graph TD
A[生成16字节随机salt] --> B{长度≥16B?}
B -- 否 --> C[panic: 长度违规]
B -- 是 --> D{Redis SETNX 唯一写入?}
D -- 失败 --> E[拒绝认证,记录审计事件]
D -- 成功 --> F[绑定至当前AuthSession]
第四章:生产级Go加盐去盐系统架构与风险防控
4.1 基于Go 1.21+内置crypto/sha256与saltedhash的零依赖封装
Go 1.21 起,crypto/sha256 性能显著提升,配合标准库 crypto/rand 可安全生成 salt,无需第三方依赖即可实现工业级密码哈希。
核心封装设计
- 使用
sha256.Sum256零分配哈希计算 - salt 长度固定为 32 字节(符合 NIST SP 800-132)
- 输出格式:
$sha256$v1$<base64-salt>$<base64-hash>
示例实现
func SaltedSHA256(password, salt []byte) string {
h := sha256.New()
h.Write(salt)
h.Write([]byte{0}) // 分隔符防长度扩展攻击
h.Write(password)
sum := h.Sum(nil)
return fmt.Sprintf("$sha256$v1$%s$%s",
base64.RawURLEncoding.EncodeToString(salt),
base64.RawURLEncoding.EncodeToString(sum))
}
逻辑说明:
h.Write([]byte{0})强制分离 salt 与口令,阻断H(salt || pwd)与H(pwd || salt)的歧义;base64.RawURLEncoding省略填充符,适配 URL/JSON 场景;返回字符串完全自描述,含算法标识、版本、salt 和哈希值。
| 组件 | 来源 | 安全特性 |
|---|---|---|
| SHA-256 | crypto/sha256 |
FIPS 180-4 认证 |
| Salt 生成 | crypto/rand |
CSPRNG,无偏置 |
| 编码 | encoding/base64 |
无填充、URL 安全 |
graph TD
A[输入明文密码] --> B[读取32字节随机salt]
B --> C[sha256.Sum256 salt+0x00+password]
C --> D[Base64编码salt与hash]
D --> E[结构化输出字符串]
4.2 并发场景下盐值生成器的sync.Pool优化与goroutine泄漏防护
盐值生成器的典型瓶颈
高并发下频繁 make([]byte, 32) 触发 GC 压力,且 rand.Read 非线程安全需加锁,成为性能热点。
sync.Pool 的正确封装
var saltPool = sync.Pool{
New: func() interface{} {
b := make([]byte, 32)
// 预分配避免扩容,不初始化内容(由使用者填充)
return b
},
}
✅ New 函数返回零值切片,避免重复分配;❌ 不在 Get 后重置长度(b[:0]),因 rand.Read 需固定容量。调用方须确保 cap(b) >= needed。
goroutine 泄漏防护要点
- 禁止在
sync.Pool.New中启动 goroutine - 所有
saltPool.Put(b)必须在 defer 或明确作用域末尾执行 - 使用
runtime.SetFinalizer辅助检测未归还对象(仅调试)
| 风险点 | 检测方式 |
|---|---|
| Pool 对象长期未归还 | pprof heap + trace |
| rand.Reader 阻塞 | go tool trace -region |
graph TD
A[Get salt from Pool] --> B{cap >= 32?}
B -->|Yes| C[Use existing buffer]
B -->|No| D[Alloc new, log warn]
C --> E[rand.Read into buffer]
E --> F[Use salt]
F --> G[Put back to Pool]
4.3 加盐凭证审计日志:结合Zap+OpenTelemetry实现可追溯合规留痕
在敏感凭证操作场景中,原始密码或密钥不得明文落盘。加盐哈希(如 sha256(salt + raw))是基础防护手段,而审计日志需完整捕获“谁、何时、对何凭证、执行了何种操作(创建/更新/删除)、加盐后摘要值、调用链上下文”。
日志结构设计
- 使用 Zap 结构化日志记录元数据
- OpenTelemetry 注入 trace_id、span_id、service.name
- 关键字段:
op="update",cred_id="cred_abc123",digest_sha256="e3b0c4...",salt_used="a1b2c3..."
示例日志采集代码
// 构建加盐摘要并注入OTel上下文
salt := "a1b2c3d4e5"
raw := "mySecretKey"
digest := fmt.Sprintf("%x", sha256.Sum256([]byte(salt+raw)))
span := trace.SpanFromContext(ctx)
logger.Info("credential_updated",
zap.String("op", "update"),
zap.String("cred_id", "cred_abc123"),
zap.String("digest_sha256", digest), // 审计唯一指纹
zap.String("salt_used", salt),
zap.String("trace_id", span.SpanContext().TraceID().String()),
)
此段代码确保凭证变更行为生成不可逆、可关联、带分布式追踪标识的审计事件;
digest_sha256作为合规留痕核心字段,避免原始凭证泄露,同时支持后续哈希比对验证完整性。
审计链路关键组件对照表
| 组件 | 职责 | 合规支撑点 |
|---|---|---|
| Zap | 结构化日志序列化与输出 | 字段可解析、防篡改格式 |
| OpenTelemetry | 分布式上下文传播与采样 | 全链路可追溯、时间锚定 |
| Salted Hash | 凭证摘要脱敏 | 满足GDPR/等保2.0存储要求 |
graph TD
A[凭证操作API] --> B{加盐哈希计算}
B --> C[Zap结构化日志]
C --> D[OTel Context注入]
D --> E[审计日志写入LTS]
E --> F[SIEM系统实时告警]
4.4 密码重哈希迁移:支持多算法共存的Go版本化Hasher Router设计
当系统需平滑升级密码哈希算法(如从 bcrypt v3 升级至 argon2id v19),直接强制重哈希会阻塞登录,而忽略旧哈希则牺牲安全性。为此,我们设计 HasherRouter——一个运行时感知密码版本、按需路由的策略型接口。
核心结构
type HasherRouter struct {
routers map[uint8]hasher // key: version byte (e.g., 0x01 → bcrypt, 0x02 → argon2)
fallback hasher // used for new password creation
}
routers 按前缀字节索引具体实现;fallback 决定新注册用户的默认算法。版本号内嵌于哈希字符串头部(如 $02$...),解耦存储与逻辑。
迁移流程
graph TD
A[用户登录] --> B{解析哈希前缀}
B -->|0x01| C[调用 bcryptv3.Verify]
B -->|0x02| D[调用 argon2id.Verify]
C & D --> E{验证成功?}
E -->|是| F[触发重哈希:用 fallback 重算并更新存储]
E -->|否| G[拒绝登录]
算法兼容性对照表
| 版本标识 | 算法 | 迭代参数 | 是否启用自动重哈希 |
|---|---|---|---|
0x01 |
bcrypt v3 | cost=12 | ✅ |
0x02 |
argon2id v19 | m=64MB, t=3, p=4 | ✅ |
0x00 |
legacy SHA1 | — | ❌(仅校验,不升级) |
第五章:Go加盐去盐不是功能需求,是合规红线:GDPR/等保2.0/PCI-DSS三级认证强制要求详解
加盐哈希为何被三大合规框架共同锁定
GDPR第32条明确要求“采用伪匿名化等适当技术措施保护个人数据”,等保2.0《基本要求》中“安全计算环境”章节(GB/T 22239—2019)强制规定:“口令等鉴别信息应以不可逆方式加密存储”;PCI-DSS v4.0第8.2.1条则直接禁止明文存储密码,并要求“使用强加密算法+唯一盐值进行哈希”。三者交汇点正是——盐值必须随机、唯一、长度≥32字节,且与哈希结果持久化分离存储。
Go标准库与第三方库的合规落差对比
| 组件 | 是否支持动态盐生成 | 盐值是否与哈希绑定存储 | 是否满足PBKDF2/Argon2最低迭代次数 | 等保2.0符合性 |
|---|---|---|---|---|
crypto/sha256 + rand.Read |
✅(需手动实现) | ❌(易误存为明文字段) | ❌(无内置迭代控制) | 不通过 |
golang.org/x/crypto/bcrypt |
✅(GenerateFromPassword自动嵌入salt) |
✅(salt与hash Base64编码合并) | ✅(cost≥12满足等保三级) | 通过 |
github.com/go-pkgz/auth/password |
✅(Hash方法强制salt参数) |
✅(返回结构体含Salt和Hash字段) |
✅(默认Argon2id, time=3, memory=64MB) | 通过 |
某支付SaaS系统被PCI-DSS现场审计驳回的真实案例
该系统使用自研md5(salt + password)方案,盐值硬编码在配置文件中,且所有用户共用同一盐。审计组调取数据库样本后指出:
- 盐值静态导致彩虹表攻击成本降低99.7%;
md5已被NIST SP 800-131A列为禁用算法;- 未实现密钥派生函数(KDF)的可调参机制(如内存/时间成本)。
整改后切换至golang.org/x/crypto/argon2.IDKey,并强制每用户独立盐值(crypto/rand.Reader生成32字节),经复审通过。
// 合规加盐实现(PCI-DSS三级 & 等保2.0双达标)
func HashPassword(password string) (hash string, salt []byte, err error) {
salt = make([]byte, 32)
if _, err = rand.Read(salt); err != nil {
return
}
// Argon2id: t=3, m=64*1024, p=2, keyLen=32
hashBytes := argon2.IDKey([]byte(password), salt, 3, 64*1024, 2, 32)
hash = base64.StdEncoding.EncodeToString(hashBytes)
return hash, salt, nil
}
盐值存储的物理隔离设计原则
等保2.0要求“鉴别数据与其他数据逻辑隔离”,实践中必须避免以下反模式:
- 将盐值作为用户表
users.salt字段直连存储; - 在Redis中用同一key同时缓存
user:123:salt与user:123:hash;
正确方案是:盐值存于专用密钥管理服务(如HashiCorp Vault KV2引擎),哈希值存业务数据库,校验时通过Vault API动态获取盐值——该架构已通过某国有银行等保三级测评。
GDPR“数据最小化”对盐值生命周期的约束
根据EDPB Guidelines 05/2021,盐值属于“为实现特定目的所必需的最少个人数据”。这意味着:
- 盐值不得记录生成时间戳(除非用于审计追踪且单独加密);
- 用户注销后,盐值必须与哈希同步销毁(非仅逻辑删除);
- 禁止将盐值用于任何非密码验证场景(如会话ID生成)。
flowchart LR
A[用户注册] --> B[生成32字节随机salt]
B --> C[调用Argon2id生成hash]
C --> D[盐值写入Vault kv2/secret/salt/<uid>]
C --> E[哈希写入MySQL users.password_hash]
F[登录请求] --> G[从Vault读取对应salt]
G --> H[Argon2id比对哈希] 