做统一认证登录
This commit is contained in:
+149
-55
@@ -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, ¶llelism); 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[:])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user