// Package user 内的 auth.go 提供与认证相关的纯函数工具。 // // 现已覆盖两组: // - 密码:HashPassword 用 argon2id 默认参数(i=1, m=64MiB, p=4, saltLen=16, // keyLen=32)签发哈希;VerifyPassword 做常量时间比对。 // - 会话 token:NewSessionToken 生成 24 字节随机原文(base64 RawURL 编码后 // 32 字符)并同时返回 sha256 摘要;HashSessionToken 是纯函数,便于服务端 // 根据请求里携带的明文 token 反查 DB 中存储的哈希。 // // 设计取舍: // - 业务规则在工具层而非 Service 层校验密码长度(minPasswordLen=8), // 以确保任何调用 HashPassword 的入口(包括 Bootstrap、改密、迁移脚本) // 都享受相同的下限,无需重复实现策略。 // - VerifyPassword 对 argon2id.ErrInvalidHash 显式返回 (false, nil): // 存量数据中如果出现非 argon2id 格式(例如历史明文或其它算法的残留), // 调用方应当作 “认证失败” 处理而非系统错误,避免登录入口因为脏数据 500。 // - Session token 明文只在登录响应里下发一次,DB 仅保存 sha256 摘要: // 即便 DB 泄漏,攻击者也无法直接拿哈希去发起认证(sha256 不可逆,且服务端 // 入口走 “明文 → 哈希反查” 路径);同时 sha256 是固定 32 字节,便于建唯一 // 索引、避免变长字段比对的歧义。 package user import ( "crypto/rand" "crypto/sha256" "encoding/base64" "errors" "fmt" "github.com/alexedwards/argon2id" ) // sessionTokenRawLen 是会话 token 的随机字节长度。 // 24 字节 = 192 bit 熵;base64 RawURL 编码后正好 32 字符且无 padding, // 既保证强度又避免 URL 转义问题。 const sessionTokenRawLen = 24 // minPasswordLen 是创建账号或修改密码时允许的最短长度。 // 8 字符是最低基线,更复杂的强度策略(字典、复用检查)留待业务侧扩展。 const minPasswordLen = 8 // HashPassword 用 argon2id 默认参数对明文密码进行哈希。 // 当明文长度不足 minPasswordLen 时返回错误,不会调用底层算法。 func HashPassword(plain string) (string, error) { if len(plain) < minPasswordLen { return "", fmt.Errorf("password must be at least %d chars", minPasswordLen) } hash, err := argon2id.CreateHash(plain, argon2id.DefaultParams) if err != nil { return "", fmt.Errorf("hash: %w", err) } return hash, nil } // VerifyPassword 将明文密码与已存哈希做常量时间比对。 // 哈希格式不合法时返回 (false, nil),让调用方按 “认证失败” 处理;其它 // 底层错误(例如参数解析失败)原样上抛。 func VerifyPassword(plain, hash string) (bool, error) { ok, err := argon2id.ComparePasswordAndHash(plain, hash) if err != nil && !errors.Is(err, argon2id.ErrInvalidHash) { return false, err } return ok, nil } // NewSessionToken 返回 (明文 token, sha256(token), err)。 // 明文只在登录响应里返回一次;DB 只存 hash,并通过 “明文 → sha256 反查” // 校验请求合法性。 func NewSessionToken() (token string, hash []byte, err error) { raw := make([]byte, sessionTokenRawLen) if _, err := rand.Read(raw); err != nil { return "", nil, fmt.Errorf("rand: %w", err) } tok := base64.RawURLEncoding.EncodeToString(raw) return tok, HashSessionToken(tok), nil } // HashSessionToken 对会话 token 明文做 sha256 摘要,返回 32 字节固定长度。 // 纯函数:相同输入恒返回相同输出,便于服务端按请求中的明文反查 DB。 func HashSessionToken(token string) []byte { sum := sha256.Sum256([]byte(token)) return sum[:] }