194 lines
5.5 KiB
Go
194 lines
5.5 KiB
Go
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
|
||
}
|
||
|