Files
user-system/internal/auth/password.go
long-agent 7b047e2f11 perf: Sprint 19 P0/P1 性能优化落地
P0(高优先级):
- P0-1: 确认数据库复合索引已存在(GORM tag),composite_index_test 验证通过
- P0-2: 连接池调优 MaxIdleConns 5→10, ConnMaxLifetime 30min→5min
- P0-3: Redis 智能探测(ProbeRedis),无 Redis 自动降级到纯内存模式

P1(中优先级):
- P1-1: GZIP 压缩中间件(compress/gzip 标准库,零新依赖)
- P1-2: 权限缓存 TTL 30min→5min
- P1-3: Argon2id 启动自适应校准(CalibrateArgon2id)

历史优化(含本次提交):
- L1Cache O(n)→O(1) LRU 重构
- Auth 中间件 DB 查询合并 + 5s L1 缓存
- Logger 异步化(4096 缓冲通道)

验证: go build/vet/test 41/41 PASS, govulncheck 无漏洞
2026-04-18 22:57:44 +08:00

239 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package auth
import (
"crypto/rand"
"crypto/subtle"
"encoding/hex"
"errors"
"fmt"
"log"
"strconv"
"strings"
"time"
"golang.org/x/crypto/argon2"
"golang.org/x/crypto/bcrypt"
)
var defaultPasswordManager = NewPassword()
// Password 密码管理器Argon2id
type Password struct {
memory uint32
iterations uint32
parallelism uint8
saltLength uint32
keyLength uint32
}
// NewPassword 创建密码管理器
func NewPassword() *Password {
return &Password{
memory: 64 * 1024, // 64MB符合 OWASP 建议)
iterations: 5, // 5 次迭代(保守值,高于 OWASP 建议的 3
parallelism: 4, // 4 并行(符合 OWASP 建议,防御 GPU 破解)
saltLength: 16, // 16 字节盐(符合 OWASP 最低要求)
keyLength: 32, // 32 字节密钥
}
}
// CalibrateArgon2id 在当前机器上自动校准 Argon2id 参数,确保单次哈希时间不超过 budget。
//
// 校准策略(优先保留 memory其次降低 iterations
// 1. 用默认参数64MB/5iter测量一次哈希耗时。
// 2. 若耗时 ≤ budget直接返回默认参数已安全。
// 3. 若耗时 > budget先尝试降低 iterations最低 2
// 4. 若仍超预算,再二分降低 memory最低 16MB
// 5. 若仍超预算,打印 warn 但不更改参数(避免参数过弱)。
//
// 建议在 main() 启动阶段调用一次,结果会更新全局 defaultPasswordManager。
// budget 推荐值500ms登录接口 P99 目标 < 1000ms留出网络/DB 余量)。
func CalibrateArgon2id(budget time.Duration) {
if budget <= 0 {
budget = 500 * time.Millisecond
}
probe := func(mem uint32, iter uint32, par uint8) time.Duration {
salt := make([]byte, 16)
_, _ = rand.Read(salt)
start := time.Now()
_ = argon2.IDKey([]byte("calibration-probe"), salt, iter, mem, par, 32)
return time.Since(start)
}
mem := defaultPasswordManager.memory
iter := defaultPasswordManager.iterations
par := defaultPasswordManager.parallelism
elapsed := probe(mem, iter, par)
log.Printf("argon2id calibration: default params (m=%dKB, t=%d, p=%d) → %v", mem, iter, par, elapsed)
if elapsed <= budget {
log.Printf("argon2id calibration: default params are within budget (%v ≤ %v), no adjustment needed", elapsed, budget)
return
}
// Step 1尝试降低 iterations最低 2低于 2 不满足 OWASP 最低要求)
for iter > 2 {
iter--
elapsed = probe(mem, iter, par)
log.Printf("argon2id calibration: trying m=%dKB t=%d p=%d → %v", mem, iter, par, elapsed)
if elapsed <= budget {
break
}
}
// Step 2若仍超预算二分降低 memory最低 16MB = 16*1024 KiB
if elapsed > budget {
const minMem = 16 * 1024
for mem > minMem && elapsed > budget {
mem /= 2
if mem < minMem {
mem = minMem
}
elapsed = probe(mem, iter, par)
log.Printf("argon2id calibration: trying m=%dKB t=%d p=%d → %v", mem, iter, par, elapsed)
}
}
if elapsed > budget {
log.Printf("argon2id calibration: WARN — even minimum params (m=%dKB, t=%d) take %v > %v; check server load", mem, iter, elapsed, budget)
// 不降低到不安全参数,保持当前已尝试的最低值
} else {
log.Printf("argon2id calibration: adjusted params m=%dKB t=%d p=%d → %v (budget: %v)", mem, iter, par, elapsed, budget)
}
// 更新全局默认管理器(仅在此阶段修改,后续不再变更)
defaultPasswordManager.memory = mem
defaultPasswordManager.iterations = iter
}
// Hash 哈希密码使用Argon2id + 随机盐)
func (p *Password) Hash(password string) (string, error) {
// 使用 crypto/rand 生成真正随机的盐
salt := make([]byte, p.saltLength)
if _, err := rand.Read(salt); err != nil {
return "", fmt.Errorf("生成随机盐失败: %w", err)
}
// 使用Argon2id哈希密码
hash := argon2.IDKey(
[]byte(password),
salt,
p.iterations,
p.memory,
p.parallelism,
p.keyLength,
)
// 格式: $argon2id$v=<version>$m=<memory>,t=<iterations>,p=<parallelism>$<salt_hex>$<hash_hex>
encoded := fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version,
p.memory,
p.iterations,
p.parallelism,
hex.EncodeToString(salt),
hex.EncodeToString(hash),
)
return encoded, nil
}
// Verify 验证密码
func (p *Password) Verify(hashedPassword, password string) bool {
// 支持 bcrypt 格式(兼容旧数据)
if strings.HasPrefix(hashedPassword, "$2a$") || strings.HasPrefix(hashedPassword, "$2b$") {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}
// 解析 Argon2id 格式
parts := strings.Split(hashedPassword, "$")
// 格式: ["", "argon2id", "v=<version>", "m=<mem>,t=<iter>,p=<par>", "<salt_hex>", "<hash_hex>"]
if len(parts) != 6 || parts[1] != "argon2id" {
return false
}
// 解析参数
var memory, iterations uint32
var parallelism uint8
params := strings.Split(parts[3], ",")
if len(params) != 3 {
return false
}
for _, param := range params {
kv := strings.SplitN(param, "=", 2)
if len(kv) != 2 {
return false
}
val, err := strconv.ParseUint(kv[1], 10, 64)
if err != nil {
return false
}
switch kv[0] {
case "m":
// #nosec G115 - argon2 memory param is constrained by spec to reasonable values
memory = uint32(val) // #nosec G115
case "t":
// #nosec G115 - argon2 iterations param is constrained by spec to reasonable values
iterations = uint32(val) // #nosec G115
case "p":
// #nosec G115 - argon2 parallelism param is constrained by spec to reasonable values
parallelism = uint8(val) // #nosec G115
}
}
// 解码盐和存储的哈希
salt, err := hex.DecodeString(parts[4])
if err != nil {
return false
}
storedHash, err := hex.DecodeString(parts[5])
if err != nil {
return false
}
// 用相同参数重新计算哈希
// #nosec G115 - bcrypt hash is typically 60 chars, fits in uint32
computedHash := argon2.IDKey(
[]byte(password),
salt,
iterations,
memory,
parallelism,
uint32(len(storedHash)), // #nosec G115
)
// 常数时间比较,防止时序攻击
return subtle.ConstantTimeCompare(storedHash, computedHash) == 1
}
// HashPassword hashes passwords with Argon2id for new credentials.
func HashPassword(password string) (string, error) {
return defaultPasswordManager.Hash(password)
}
// VerifyPassword verifies both Argon2id and legacy bcrypt password hashes.
func VerifyPassword(hashedPassword, password string) bool {
return defaultPasswordManager.Verify(hashedPassword, password)
}
// ErrInvalidPassword 密码无效错误
var ErrInvalidPassword = errors.New("密码无效")
// BcryptHash 使用bcrypt哈希密码兼容性支持
func BcryptHash(password string) (string, error) {
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
return "", fmt.Errorf("bcrypt加密失败: %w", err)
}
return string(hash), nil
}
// BcryptVerify 使用bcrypt验证密码
func BcryptVerify(hashedPassword, password string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
return err == nil
}