feat(user): argon2id password hashing helpers

This commit is contained in:
2026-04-28 15:46:23 +08:00
parent 46fda4f594
commit da316725fc
4 changed files with 117 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
// Package user 内的 auth.go 提供与认证相关的纯函数工具。
//
// 当前阶段(E2)只覆盖密码哈希:HashPassword 用 argon2id 默认参数(i=1,
// m=64MiB, p=4, saltLen=16, keyLen=32)签发哈希;VerifyPassword 做常量时间
// 比对。后续 E3 步骤会在同文件追加 SessionToken 相关逻辑(生成原文 + sha256
// 摘要),届时这两组工具会与 Service 层组装出完整的登录链路。
//
// 设计取舍:
// - 业务规则在工具层而非 Service 层校验密码长度(minPasswordLen=8),
// 以确保任何调用 HashPassword 的入口(包括 Bootstrap、改密、迁移脚本)
// 都享受相同的下限,无需重复实现策略。
// - VerifyPassword 对 argon2id.ErrInvalidHash 显式返回 (false, nil):
// 存量数据中如果出现非 argon2id 格式(例如历史明文或其它算法的残留),
// 调用方应当作 “认证失败” 处理而非系统错误,避免登录入口因为脏数据 500。
package user
import (
"errors"
"fmt"
"github.com/alexedwards/argon2id"
)
// 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
}
+26
View File
@@ -0,0 +1,26 @@
package user
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestHashAndVerifyPassword(t *testing.T) {
hash, err := HashPassword("hello-world-12345")
require.NoError(t, err)
require.NotEmpty(t, hash)
ok, err := VerifyPassword("hello-world-12345", hash)
require.NoError(t, err)
require.True(t, ok)
bad, err := VerifyPassword("wrong", hash)
require.NoError(t, err)
require.False(t, bad)
}
func TestHashPassword_RejectsTooShort(t *testing.T) {
_, err := HashPassword("short")
require.Error(t, err)
}