做统一认证登录

This commit is contained in:
2026-09-19 21:44:04 +08:00
parent 1a4471e34d
commit fa281363fb
78 changed files with 8127 additions and 1442 deletions
+149 -55
View File
@@ -1,55 +1,149 @@
package passwordutil
import (
"crypto/rand"
"crypto/sha256"
"encoding/hex"
"errors"
"strings"
)
const (
saltBytes = 16
separator = "$"
hashLength = 64 // sha256 hex length
)
// Hash 生成 salt+hash 的存储串,格式:salt$hash(均为 hex)
func Hash(plain string) (string, error) {
plain = strings.TrimSpace(plain)
if plain == "" {
return "", errors.New("password 不能为空")
}
salt := make([]byte, saltBytes)
if _, err := rand.Read(salt); err != nil {
return "", err
}
saltHex := hex.EncodeToString(salt)
hashHex := hashHex(saltHex, plain)
return saltHex + separator + hashHex, nil
}
// Verify 校验存储串(salt$hash)是否匹配输入明文密码。
func Verify(stored, plain string) bool {
stored = strings.TrimSpace(stored)
plain = strings.TrimSpace(plain)
if stored == "" || plain == "" {
return false
}
parts := strings.Split(stored, separator)
if len(parts) != 2 {
return false
}
saltHex := strings.TrimSpace(parts[0])
hashHexStored := strings.TrimSpace(parts[1])
if saltHex == "" || len(hashHexStored) != hashLength {
return false
}
return hashHex(saltHex, plain) == strings.ToLower(hashHexStored)
}
func hashHex(saltHex, plain string) string {
sum := sha256.Sum256([]byte(saltHex + plain))
return hex.EncodeToString(sum[:])
}
package passwordutil
import (
"crypto/rand"
"crypto/sha256"
"crypto/subtle"
"encoding/base64"
"encoding/hex"
"errors"
"fmt"
"strings"
"golang.org/x/crypto/argon2"
)
// 算法标识。
// 历史数据用 legacy(sha256 单轮,无迭代拉伸,抗 GPU 爆破能力弱);
// 新密码统一用 argon2id,登录成功检测到 legacy 时应重新哈希升级。
const (
AlgoArgon2id = "argon2id"
AlgoLegacy = "legacy"
)
// argon2id 参数(OWASP 推荐起步配置:64MB / 3 轮 / 并行 2)
const (
argonTime = 3
argonMemory = 64 * 1024 // 单位 KB,即 64MB
argonThreads = 2
argonKeyLen = 32
argonSaltLen = 16
)
// legacy 兼容参数
const (
saltBytes = 16
separator = "$"
hashLength = 64 // sha256 hex length
)
// Hash 使用当前默认算法(argon2id)生成密码存储串。
//
// 返回 PHC 标准格式(盐与参数内联,无需独立 salt 列):
//
// $argon2id$v=19$m=65536,t=3,p=2$<base64(salt)>$<base64(hash)>
//
// 注意:函数签名与旧版一致,全部调用点无需改动即可切换到新算法。
func Hash(plain string) (string, error) {
plain = strings.TrimSpace(plain)
if plain == "" {
return "", errors.New("password 不能为空")
}
salt := make([]byte, argonSaltLen)
if _, err := rand.Read(salt); err != nil {
return "", err
}
key := argon2.IDKey([]byte(plain), salt, argonTime, argonMemory, argonThreads, argonKeyLen)
return fmt.Sprintf("$argon2id$v=%d$m=%d,t=%d,p=%d$%s$%s",
argon2.Version,
argonMemory, argonTime, argonThreads,
base64.RawStdEncoding.EncodeToString(salt),
base64.RawStdEncoding.EncodeToString(key),
), nil
}
// Verify 校验明文密码是否匹配存储串,自动识别算法:
// - $argon2id$... → argon2id(新)
// - <salt>$<hash> → legacy sha256(旧,兼容)
func Verify(stored, plain string) bool {
stored = strings.TrimSpace(stored)
plain = strings.TrimSpace(plain)
if stored == "" || plain == "" {
return false
}
if strings.HasPrefix(stored, "$argon2id$") {
return verifyArgon2id(stored, plain)
}
return verifyLegacy(stored, plain)
}
// NeedsRehash 判断已存储的密码是否需要用当前算法重新哈希。
// 登录成功后调用:返回 true 时应拿当次登录的明文重新 Hash 并落库,
// 实现旧算法用户「首次登录自动升级」,无需强制全员重置密码。
func NeedsRehash(stored string) bool {
return !strings.HasPrefix(strings.TrimSpace(stored), "$argon2id$")
}
// AlgoOf 返回存储串使用的算法标识,用于审计/统计旧算法存量。
func AlgoOf(stored string) string {
if strings.HasPrefix(strings.TrimSpace(stored), "$argon2id$") {
return AlgoArgon2id
}
return AlgoLegacy
}
// verifyArgon2id 解析 PHC 串并用相同参数重算比对(恒定时间比较)。
func verifyArgon2id(stored, plain string) bool {
parts := strings.Split(stored, "$")
// ["", "argon2id", "v=19", "m=...,t=...,p=...", salt, hash]
if len(parts) != 6 {
return false
}
var memory, iterations, parallelism uint32
if _, err := fmt.Sscanf(parts[3], "m=%d,t=%d,p=%d", &memory, &iterations, &parallelism); err != nil {
return false
}
if memory == 0 || iterations == 0 || parallelism == 0 || parallelism > 255 {
return false
}
threads := uint8(parallelism)
salt, err := base64.RawStdEncoding.DecodeString(parts[4])
if err != nil || len(salt) == 0 {
return false
}
want, err := base64.RawStdEncoding.DecodeString(parts[5])
if err != nil || len(want) == 0 {
return false
}
got := argon2.IDKey([]byte(plain), salt, iterations, memory, threads, uint32(len(want)))
return subtle.ConstantTimeCompare(got, want) == 1
}
// verifyLegacy 校验旧格式:salt$hash(均为 hex),sha256(salt+plain)。
// 仅用于兼容历史数据,不再用于新密码。
func verifyLegacy(stored, plain string) bool {
stored = strings.TrimSpace(stored)
plain = strings.TrimSpace(plain)
if stored == "" || plain == "" {
return false
}
parts := strings.Split(stored, separator)
if len(parts) != 2 {
return false
}
saltHex := strings.TrimSpace(parts[0])
hashHexStored := strings.TrimSpace(parts[1])
if saltHex == "" || len(hashHexStored) != hashLength {
return false
}
if _, err := hex.DecodeString(saltHex); err != nil {
return false
}
return hashHex(saltHex, plain) == strings.ToLower(hashHexStored)
}
// hashHex 旧算法:sha256(saltHex + plain),供 verifyLegacy 使用。
func hashHex(saltHex, plain string) string {
sum := sha256.Sum256([]byte(saltHex + plain))
return hex.EncodeToString(sum[:])
}