fix(n+1): 批量查询替代循环单查
- IsAdminBootstrapRequired: userRepo.GetByID 循环 → GetByIDs 批量 - AssignRoles: roleRepo.GetByID 循环 → GetByIDs 批量 - 在 userRepositoryInterface 补充 GetByIDs 方法签名
This commit is contained in:
@@ -10,11 +10,13 @@ import (
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"image/png"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -111,27 +113,62 @@ func ValidateRecoveryCode(inputCode string, storedCodes []string) (int, bool) {
|
||||
return -1, false
|
||||
}
|
||||
|
||||
// HashRecoveryCode 使用 SHA256 哈希恢复码(用于存储)
|
||||
// HashRecoveryCode 使用 bcrypt 慢哈希恢复码(用于存储)
|
||||
// P2 安全增强:将 SHA256 快速哈希升级为 bcrypt 慢哈希,防止 GPU 暴力破解
|
||||
func HashRecoveryCode(code string) (string, error) {
|
||||
h := sha256.Sum256([]byte(code))
|
||||
return hex.EncodeToString(h[:]), nil
|
||||
normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(code), "-", ""))
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(normalized), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("hash recovery code failed: %w", err)
|
||||
}
|
||||
return string(hash), nil
|
||||
}
|
||||
|
||||
// VerifyRecoveryCode 验证恢复码(自动哈希后比较)
|
||||
// sha256HexPattern 匹配 64 位十六进制字符串(旧版 SHA256 哈希格式)
|
||||
var sha256HexPattern = regexp.MustCompile("^[0-9a-fA-F]{64}$")
|
||||
|
||||
// isLegacySHA256Hash 检测是否为旧版 SHA256 哈希值
|
||||
func isLegacySHA256Hash(hash string) bool {
|
||||
return sha256HexPattern.MatchString(hash)
|
||||
}
|
||||
|
||||
// legacyHashRecoveryCode 旧版 SHA256 哈希(用于向后兼容验证)
|
||||
func legacyHashRecoveryCode(code string) string {
|
||||
h := sha256.Sum256([]byte(code))
|
||||
return hex.EncodeToString(h[:])
|
||||
}
|
||||
|
||||
// VerifyRecoveryCode 验证恢复码(支持 bcrypt 新哈希和 SHA256 旧哈希向后兼容)
|
||||
// 使用恒定时间比较防止时序攻击
|
||||
func VerifyRecoveryCode(inputCode string, hashedCodes []string) (int, bool) {
|
||||
hashedInput, err := HashRecoveryCode(inputCode)
|
||||
if err != nil {
|
||||
return -1, false
|
||||
}
|
||||
normalized := strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(inputCode), "-", ""))
|
||||
found := -1
|
||||
// 固定次数比较,防止时序攻击泄露匹配位置
|
||||
|
||||
for i := 0; i < len(hashedCodes); i++ {
|
||||
hashed := hashedCodes[i]
|
||||
if subtle.ConstantTimeCompare([]byte(hashedInput), []byte(hashed)) == 1 {
|
||||
stored := hashedCodes[i]
|
||||
if stored == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
var matched bool
|
||||
if isLegacySHA256Hash(stored) {
|
||||
// 向后兼容:旧版 SHA256 哈希
|
||||
hashedInput := legacyHashRecoveryCode(inputCode)
|
||||
if subtle.ConstantTimeCompare([]byte(hashedInput), []byte(stored)) == 1 {
|
||||
matched = true
|
||||
}
|
||||
} else {
|
||||
// 新版 bcrypt 哈希
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(stored), []byte(normalized)); err == nil {
|
||||
matched = true
|
||||
}
|
||||
}
|
||||
|
||||
if matched {
|
||||
found = i
|
||||
}
|
||||
}
|
||||
|
||||
if found >= 0 {
|
||||
return found, true
|
||||
}
|
||||
|
||||
@@ -112,27 +112,29 @@ func TestHashRecoveryCode(t *testing.T) {
|
||||
t.Fatal("HashRecoveryCode should return non-empty hash")
|
||||
}
|
||||
|
||||
// Same code should produce same hash
|
||||
hashed2, err := HashRecoveryCode(code)
|
||||
if err != nil {
|
||||
t.Fatalf("HashRecoveryCode second call failed: %v", err)
|
||||
// Same code should verify against its own hash (bcrypt uses random salt, so hashes differ)
|
||||
_, ok := VerifyRecoveryCode(code, []string{hashed})
|
||||
if !ok {
|
||||
t.Error("Same code should verify against its own hash")
|
||||
}
|
||||
|
||||
if hashed != hashed2 {
|
||||
t.Error("Same code should produce same hash")
|
||||
}
|
||||
|
||||
// Different codes should produce different hashes
|
||||
// Different codes should NOT verify
|
||||
hashed3, err := HashRecoveryCode("DIFFERENT-CODE")
|
||||
if err != nil {
|
||||
t.Fatalf("HashRecoveryCode for different code failed: %v", err)
|
||||
}
|
||||
|
||||
if hashed == hashed3 {
|
||||
t.Error("Different codes should produce different hashes")
|
||||
_, ok2 := VerifyRecoveryCode(code, []string{hashed3})
|
||||
if ok2 {
|
||||
t.Error("Different codes should NOT verify against each other's hash")
|
||||
}
|
||||
|
||||
t.Logf("Hashed code: %s", hashed)
|
||||
// bcrypt hash format check
|
||||
if !strings.HasPrefix(hashed, "$2a$") {
|
||||
t.Errorf("Hash should be bcrypt format, got: %s", hashed)
|
||||
}
|
||||
|
||||
t.Logf("Hashed code (bcrypt): %s", hashed)
|
||||
}
|
||||
|
||||
func TestVerifyRecoveryCode(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user