init
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image/png"
|
||||
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
|
||||
"github.com/goravel/framework/facades"
|
||||
|
||||
"goravel/app/utils/errorlog"
|
||||
)
|
||||
|
||||
type GoogleAuthenticatorService interface {
|
||||
// GenerateSecret 生成密钥
|
||||
GenerateSecret(accountName string) (secret string, qrCodeURL string, err error)
|
||||
// GenerateQRCodeImage 生成二维码图片(base64)
|
||||
GenerateQRCodeImage(accountName, secret string) (string, error)
|
||||
// Verify 验证验证码
|
||||
Verify(secret, code string) bool
|
||||
// IsBound 检查管理员是否绑定了谷歌验证码
|
||||
IsBound(adminID uint) (bool, error)
|
||||
// GetSecret 获取管理员的密钥(用于绑定确认)
|
||||
GetSecret(adminID uint) (string, error)
|
||||
// Bind 绑定谷歌验证码
|
||||
Bind(adminID uint, secret, code string) error
|
||||
// Unbind 解绑谷歌验证码
|
||||
Unbind(adminID uint) error
|
||||
}
|
||||
|
||||
type GoogleAuthenticatorServiceImpl struct {
|
||||
}
|
||||
|
||||
func NewGoogleAuthenticatorServiceImpl() GoogleAuthenticatorService {
|
||||
return &GoogleAuthenticatorServiceImpl{}
|
||||
}
|
||||
|
||||
// GenerateSecret 生成密钥
|
||||
func (s *GoogleAuthenticatorServiceImpl) GenerateSecret(accountName string) (secret string, qrCodeURL string, err error) {
|
||||
// 获取应用名称(从配置中读取,如果没有则使用默认值)
|
||||
appName := facades.Config().GetString("app.name", "Goravel Admin")
|
||||
if appName == "" {
|
||||
appName = "Goravel Admin"
|
||||
}
|
||||
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: appName,
|
||||
AccountName: accountName,
|
||||
})
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
// 生成二维码URL
|
||||
qrCodeURL = key.URL()
|
||||
|
||||
return key.Secret(), qrCodeURL, nil
|
||||
}
|
||||
|
||||
// GenerateQRCodeImage 生成二维码图片(base64)
|
||||
// 使用标准的TOTP格式(RFC 6238),完全兼容Google Authenticator
|
||||
func (s *GoogleAuthenticatorServiceImpl) GenerateQRCodeImage(accountName, secret string) (string, error) {
|
||||
appName := facades.Config().GetString("app.name", "Goravel Admin")
|
||||
if appName == "" {
|
||||
appName = "Goravel Admin"
|
||||
}
|
||||
|
||||
// 构建标准的otpauth URL(Google Authenticator标准格式)
|
||||
// 格式:otpauth://totp/Issuer:AccountName?secret=SECRET&issuer=Issuer
|
||||
otpURL := fmt.Sprintf("otpauth://totp/%s:%s?secret=%s&issuer=%s",
|
||||
appName, accountName, secret, appName)
|
||||
|
||||
// 从URL创建key对象(这样可以确保格式完全符合标准)
|
||||
key, err := otp.NewKeyFromURL(otpURL)
|
||||
if err != nil {
|
||||
errorlog.Record(context.Background(), "google-authenticator", "创建密钥失败", map[string]any{
|
||||
"app_name": appName,
|
||||
"account_name": accountName,
|
||||
"error": err.Error(),
|
||||
}, "failed to create key from URL: %w", err)
|
||||
return "", fmt.Errorf("failed to create key from URL: %w", err)
|
||||
}
|
||||
|
||||
// 生成二维码图片(200x200像素,Google Authenticator推荐尺寸)
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(200, 200)
|
||||
if err != nil {
|
||||
errorlog.Record(context.Background(), "google-authenticator", "生成二维码图片失败", map[string]any{
|
||||
"app_name": appName,
|
||||
"account_name": accountName,
|
||||
"error": err.Error(),
|
||||
}, "failed to generate QR code image: %w", err)
|
||||
return "", fmt.Errorf("failed to generate QR code image: %w", err)
|
||||
}
|
||||
|
||||
// 将图片编码为PNG
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
errorlog.Record(context.Background(), "google-authenticator", "编码PNG失败", map[string]any{
|
||||
"app_name": appName,
|
||||
"account_name": accountName,
|
||||
"error": err.Error(),
|
||||
}, "failed to encode PNG: %w", err)
|
||||
return "", fmt.Errorf("failed to encode PNG: %w", err)
|
||||
}
|
||||
|
||||
// 转换为base64
|
||||
base64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
|
||||
return "data:image/png;base64," + base64Str, nil
|
||||
}
|
||||
|
||||
// Verify 验证验证码
|
||||
func (s *GoogleAuthenticatorServiceImpl) Verify(secret, code string) bool {
|
||||
if secret == "" || code == "" {
|
||||
return false
|
||||
}
|
||||
return totp.Validate(code, secret)
|
||||
}
|
||||
|
||||
// IsBound 检查管理员是否绑定了谷歌验证码
|
||||
func (s *GoogleAuthenticatorServiceImpl) IsBound(adminID uint) (bool, error) {
|
||||
// 先检查列是否存在
|
||||
columns, err := facades.Schema().GetColumns("admins")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
hasGoogleSecretColumn := false
|
||||
for _, column := range columns {
|
||||
if column.Name == "google_secret" {
|
||||
hasGoogleSecretColumn = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 如果列不存在,返回 false(未绑定)
|
||||
if !hasGoogleSecretColumn {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// 列存在,检查是否有值
|
||||
count, err := facades.Orm().Query().Table("admins").
|
||||
Where("id", adminID).
|
||||
Where("google_secret IS NOT NULL").
|
||||
Where("google_secret != ?", "").
|
||||
Count()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
// GetSecret 获取管理员的密钥(用于绑定确认)
|
||||
func (s *GoogleAuthenticatorServiceImpl) GetSecret(adminID uint) (string, error) {
|
||||
var admin struct {
|
||||
GoogleSecret string
|
||||
}
|
||||
err := facades.Orm().Query().Table("admins").
|
||||
Select("google_secret").
|
||||
Where("id", adminID).
|
||||
First(&admin)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return admin.GoogleSecret, nil
|
||||
}
|
||||
|
||||
// Bind 绑定谷歌验证码
|
||||
func (s *GoogleAuthenticatorServiceImpl) Bind(adminID uint, secret, code string) error {
|
||||
// 先验证验证码是否正确
|
||||
if !s.Verify(secret, code) {
|
||||
return fmt.Errorf("invalid_code")
|
||||
}
|
||||
|
||||
// 更新管理员的google_secret
|
||||
_, err := facades.Orm().Query().Table("admins").
|
||||
Where("id", adminID).
|
||||
Update("google_secret", secret)
|
||||
return err
|
||||
}
|
||||
|
||||
// Unbind 解绑谷歌验证码
|
||||
func (s *GoogleAuthenticatorServiceImpl) Unbind(adminID uint) error {
|
||||
_, err := facades.Orm().Query().Table("admins").
|
||||
Where("id", adminID).
|
||||
Update("google_secret", nil)
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user