totp.go 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  1. package common
  2. import (
  3. "crypto/rand"
  4. "fmt"
  5. "os"
  6. "strconv"
  7. "strings"
  8. "github.com/pquerna/otp"
  9. "github.com/pquerna/otp/totp"
  10. )
  11. const (
  12. // 备用码配置
  13. BackupCodeLength = 8 // 备用码长度
  14. BackupCodeCount = 4 // 生成备用码数量
  15. // 限制配置
  16. MaxFailAttempts = 5 // 最大失败尝试次数
  17. LockoutDuration = 300 // 锁定时间(秒)
  18. )
  19. // GenerateTOTPSecret 生成TOTP密钥和配置
  20. func GenerateTOTPSecret(accountName string) (*otp.Key, error) {
  21. issuer := Get2FAIssuer()
  22. return totp.Generate(totp.GenerateOpts{
  23. Issuer: issuer,
  24. AccountName: accountName,
  25. Period: 30,
  26. Digits: otp.DigitsSix,
  27. Algorithm: otp.AlgorithmSHA1,
  28. })
  29. }
  30. // ValidateTOTPCode 验证TOTP验证码
  31. func ValidateTOTPCode(secret, code string) bool {
  32. // 清理验证码格式
  33. cleanCode := strings.ReplaceAll(code, " ", "")
  34. if len(cleanCode) != 6 {
  35. return false
  36. }
  37. // 验证验证码
  38. return totp.Validate(cleanCode, secret)
  39. }
  40. // GenerateBackupCodes 生成备用恢复码
  41. func GenerateBackupCodes() ([]string, error) {
  42. codes := make([]string, BackupCodeCount)
  43. for i := 0; i < BackupCodeCount; i++ {
  44. code, err := generateRandomBackupCode()
  45. if err != nil {
  46. return nil, err
  47. }
  48. codes[i] = code
  49. }
  50. return codes, nil
  51. }
  52. // generateRandomBackupCode 生成单个备用码
  53. func generateRandomBackupCode() (string, error) {
  54. const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
  55. code := make([]byte, BackupCodeLength)
  56. for i := range code {
  57. randomBytes := make([]byte, 1)
  58. _, err := rand.Read(randomBytes)
  59. if err != nil {
  60. return "", err
  61. }
  62. code[i] = charset[int(randomBytes[0])%len(charset)]
  63. }
  64. // 格式化为 XXXX-XXXX 格式
  65. return fmt.Sprintf("%s-%s", string(code[:4]), string(code[4:])), nil
  66. }
  67. // ValidateBackupCode 验证备用码格式
  68. func ValidateBackupCode(code string) bool {
  69. // 移除所有分隔符并转为大写
  70. cleanCode := strings.ToUpper(strings.ReplaceAll(code, "-", ""))
  71. if len(cleanCode) != BackupCodeLength {
  72. return false
  73. }
  74. // 检查字符是否合法
  75. for _, char := range cleanCode {
  76. if !((char >= 'A' && char <= 'Z') || (char >= '0' && char <= '9')) {
  77. return false
  78. }
  79. }
  80. return true
  81. }
  82. // NormalizeBackupCode 标准化备用码格式
  83. func NormalizeBackupCode(code string) string {
  84. cleanCode := strings.ToUpper(strings.ReplaceAll(code, "-", ""))
  85. if len(cleanCode) == BackupCodeLength {
  86. return fmt.Sprintf("%s-%s", cleanCode[:4], cleanCode[4:])
  87. }
  88. return code
  89. }
  90. // HashBackupCode 对备用码进行哈希
  91. func HashBackupCode(code string) (string, error) {
  92. normalizedCode := NormalizeBackupCode(code)
  93. return Password2Hash(normalizedCode)
  94. }
  95. // Get2FAIssuer 获取2FA发行者名称
  96. func Get2FAIssuer() string {
  97. return SystemName
  98. }
  99. // getEnvOrDefault 获取环境变量或默认值
  100. func getEnvOrDefault(key, defaultValue string) string {
  101. if value, exists := os.LookupEnv(key); exists {
  102. return value
  103. }
  104. return defaultValue
  105. }
  106. // ValidateNumericCode 验证数字验证码格式
  107. func ValidateNumericCode(code string) (string, error) {
  108. // 移除空格
  109. code = strings.ReplaceAll(code, " ", "")
  110. if len(code) != 6 {
  111. return "", fmt.Errorf("验证码必须是6位数字")
  112. }
  113. // 检查是否为纯数字
  114. if _, err := strconv.Atoi(code); err != nil {
  115. return "", fmt.Errorf("验证码只能包含数字")
  116. }
  117. return code, nil
  118. }
  119. // GenerateQRCodeData 生成二维码数据
  120. func GenerateQRCodeData(secret, username string) string {
  121. issuer := Get2FAIssuer()
  122. accountName := fmt.Sprintf("%s (%s)", username, issuer)
  123. return fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&digits=6&period=30",
  124. issuer, accountName, secret, issuer)
  125. }