feat: twofactor bind

This commit is contained in:
henry.chen
2025-07-17 15:24:46 +08:00
parent 91e1731909
commit e5100fa018
11 changed files with 125 additions and 19 deletions

View File

@@ -9,11 +9,13 @@ import (
"strings"
"time"
"github.com/eiblog/eiblog/cmd/eiblog/config"
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal"
"github.com/eiblog/eiblog/pkg/middleware"
"github.com/eiblog/eiblog/pkg/model"
"github.com/eiblog/eiblog/pkg/third/qiniu"
"github.com/eiblog/eiblog/tools"
"github.com/pquerna/otp/totp"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
@@ -46,13 +48,14 @@ func RegisterRoutesAuthz(group gin.IRoutes) {
group.POST("/api/trash-recover", handleAPITrashRecover)
group.POST("/api/file-upload", handleAPIQiniuUpload)
group.POST("/api/file-delete", handleAPIQiniuDelete)
group.POST("/api/twofactor", handleAPITwoFactor)
}
// handleAcctLogin 登录接口
func handleAcctLogin(c *gin.Context) {
user := c.PostForm("user")
pwd := c.PostForm("password")
// code := c.PostForm("code") // 二次验证
code := c.PostForm("code")
if user == "" || pwd == "" {
logrus.Warnf("参数错误: %s %s", user, pwd)
c.Redirect(http.StatusFound, "/admin/login")
@@ -64,6 +67,16 @@ func handleAcctLogin(c *gin.Context) {
c.Redirect(http.StatusFound, "/admin/login")
return
}
// 两步验证
if config.Conf.General.TwoFactor &&
internal.Ei.Account.TwoFactorSecret != "" {
valid := totp.Validate(code, internal.Ei.Account.TwoFactorSecret)
if !valid {
logrus.Warnf("两步验证: %s", code)
c.Redirect(http.StatusFound, "/admin/login")
return
}
}
// 登录成功
middleware.SetLogin(c, user)
@@ -76,6 +89,33 @@ func handleAcctLogin(c *gin.Context) {
c.Redirect(http.StatusFound, "/admin/profile")
}
// handleAPITwoFactor 两步验证
func handleAPITwoFactor(c *gin.Context) {
code := c.PostForm("code")
if code == "" {
responseNotice(c, NoticeNotice, "验证码不能为空", "")
return
}
valid := totp.Validate(code, internal.TwoFactorSecret)
if !valid {
responseNotice(c, NoticeNotice, "验证码错误", "")
return
}
err := internal.Store.UpdateAccount(context.Background(), internal.Ei.Account.Username,
map[string]interface{}{
"two_factor_secret": internal.TwoFactorSecret,
})
if err != nil {
logrus.Error("handleAPITwoFactor.UpdateAccount: ", err)
responseNotice(c, NoticeNotice, err.Error(), "")
return
}
internal.Ei.Account.TwoFactorSecret = internal.TwoFactorSecret
internal.TwoFactorSecret = ""
c.Request.Header.Set("Referer", "/admin/profile")
responseNotice(c, NoticeSuccess, "绑定成功", "")
}
// handleAPIBlogger 更新博客信息
func handleAPIBlogger(c *gin.Context) {
bn := c.PostForm("blogName")

View File

@@ -22,8 +22,9 @@ var (
XMLTemplate *template.Template // template/xml模板
HTMLTemplate *template.Template // website/html模板
Store store.Store // 数据库存储
Ei *Cache // 博客数据缓存
Store store.Store // 数据库存储
Ei *Cache // 博客数据缓存
TwoFactorSecret string // 缓存两步验证密钥
ESClient *es.ESClient // es 客户端
DisqusClient *disqus.DisqusClient // disqus 客户端

View File

@@ -8,6 +8,7 @@ import (
"github.com/eiblog/eiblog/pkg/model"
"github.com/sirupsen/logrus"
"gorm.io/driver/clickhouse"
"gorm.io/driver/mysql"
"gorm.io/driver/postgres"
@@ -63,12 +64,15 @@ func (db *rdbms) Init(name, source string) (Store, error) {
return nil, err
}
// auto migrate
gormDB.AutoMigrate(
err = gormDB.AutoMigrate(
&model.Account{},
&model.Blogger{},
&model.Article{},
&model.Serie{},
)
if err != nil {
logrus.Error("rdbms.AutoMigrate: ", err)
}
db.DB = gormDB
return db, nil
}

View File

@@ -3,9 +3,11 @@ package page
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"html/template"
"image/png"
"net/http"
"strconv"
@@ -13,6 +15,7 @@ import (
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal"
"github.com/eiblog/eiblog/cmd/eiblog/handler/internal/store"
"github.com/eiblog/eiblog/pkg/middleware"
"github.com/pquerna/otp/totp"
"github.com/gin-gonic/gin"
"github.com/sirupsen/logrus"
@@ -35,7 +38,11 @@ func handleLoginPage(c *gin.Context) {
c.Redirect(http.StatusFound, "/admin/profile")
return
}
params := gin.H{"BTitle": internal.Ei.Blogger.BTitle}
params := gin.H{
"BTitle": internal.Ei.Blogger.BTitle,
"TwoFactor": config.Conf.General.TwoFactor &&
internal.Ei.Account.TwoFactorSecret != "",
}
renderHTMLAdminLayout(c, "login.html", params)
}
@@ -46,6 +53,31 @@ func handleAdminProfile(c *gin.Context) {
params["Path"] = c.Request.URL.Path
params["Console"] = true
params["Ei"] = internal.Ei
if c.Query("unbind") == "true" {
internal.Ei.Account.TwoFactorSecret = ""
_ = internal.Store.UpdateAccount(context.Background(), internal.Ei.Account.Username,
map[string]interface{}{
"two_factor_secret": "",
})
}
if config.Conf.General.TwoFactor &&
internal.Ei.Account.TwoFactorSecret == "" {
key, err := totp.Generate(totp.GenerateOpts{
Issuer: config.Conf.Host,
AccountName: internal.Ei.Account.Username,
})
internal.TwoFactorSecret = key.Secret()
if err == nil {
var buf bytes.Buffer
img, err := key.Image(200, 200)
if err != nil {
panic(err)
}
png.Encode(&buf, img)
b64 := base64.StdEncoding.EncodeToString(buf.Bytes())
params["TwoFactorSecret"] = "data:image/png;base64," + b64
}
}
renderHTMLAdminLayout(c, "admin-profile", params)
}