Published on

golang实现2fa(otp)验证

Authors
  • avatar
    Name
    liuxiaobo
    Twitter
2fa

totp.go

package auth

import (
	"crypto/rand"
	"encoding/base32"
	"fmt"
	"time"

	"github.com/pquerna/otp"
	"github.com/pquerna/otp/totp"
)

// TOTPConfig 包含 TOTP 配置信息
type TOTPConfig struct {
	Secret string
	Period uint
}

// GenerateSecret 生成新的 TOTP 密钥
func GenerateSecret(s string) (*TOTPConfig, error) {
	var secret string

	if s != "" {
		secret = s
	} else {
		// 生成随机密钥
		bytes := make([]byte, 20)

		if _, err := rand.Read(bytes); err != nil {
			return nil, fmt.Errorf("生成随机密钥失败: %v", err)
		}

		// 将密钥转换为 base32 格式
		secret = base32.StdEncoding.EncodeToString(bytes)
	}

	return &TOTPConfig{
		Secret: secret,
		Period: 30, // 验证码有效期为 30 秒
	}, nil
}

// GenerateCode 根据密钥生成当前的验证码
func (c *TOTPConfig) GenerateCode() (string, error) {
	code, err := totp.GenerateCode(c.Secret, time.Now())
	if err != nil {
		return "", fmt.Errorf("生成验证码失败: %v", err)
	}
	return code, nil
}

// ValidateCode 验证用户输入的验证码是否正确
func (c *TOTPConfig) ValidateCode(code string) bool {
	return totp.Validate(code, c.Secret)
}

// GetQRCodeURL 生成可用于扫描的二维码 URL
func (c *TOTPConfig) GetQRCodeURL(accountName, issuer string) string {
	key, err := otp.NewKeyFromURL(fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s&period=%d",
		issuer,
		accountName,
		c.Secret,
		issuer,
		c.Period))
	if err != nil {
		return ""
	}
	return key.URL()
}

main.go

package main

import (
	"fmt"
	"time"

	"my_otp/auth"
)

// otpauth://totp/GitHub:liuxiaobopro?secret=4MEE7Q2BZBD4G6NC&issuer=GitHub
func main() {
	// 1. 生成新的 TOTP 配置
	config, err := auth.GenerateSecret("4MEE7Q2BZBD4G6NC")
	if err != nil {
		panic(err)
	}

	// 打印密钥(在实际应用中应该安全存储)
	fmt.Printf("密钥: %s\n", config.Secret)

	// 2. 生成二维码 URL(可用于生成 QR 码供手机扫描)
	qrURL := config.GetQRCodeURL("GitHub:liuxiaobopro", "GitHub")
	fmt.Printf("二维码 URL: %s\n", qrURL)

	// 3. 生成当前验证码
	code, err := config.GenerateCode()
	if err != nil {
		panic(err)
	}
	fmt.Printf("当前验证码: %s\n", code)

	// 4. 验证码验证
	isValid := config.ValidateCode(code)
	fmt.Printf("验证结果: %v\n", isValid)

	// 等待几秒后再次生成验证码,演示码会变化
	time.Sleep(31 * time.Second)

	for range time.Tick(time.Second * 30) {
		newCode, _ := config.GenerateCode()
		fmt.Printf("新的验证码: %s\n", newCode)
	}
}