fix(n+1): 批量查询替代循环单查

- IsAdminBootstrapRequired: userRepo.GetByID 循环 → GetByIDs 批量
- AssignRoles: roleRepo.GetByID 循环 → GetByIDs 批量
- 在 userRepositoryInterface 补充 GetByIDs 方法签名
This commit is contained in:
2026-05-08 08:05:26 +08:00
parent 9b1cea246e
commit 2a18a6fb47
39 changed files with 3169 additions and 393 deletions

View File

@@ -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
}

View File

@@ -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) {