mirror of
https://github.com/eiblog/eiblog.git
synced 2026-02-07 23:32:27 +08:00
feat: twofactor bind
This commit is contained in:
@@ -19,6 +19,7 @@ general: # 常规配置
|
||||
identifier: <!--more--> # 截取预览标识
|
||||
length: 400 # 自动截取预览, 字符数
|
||||
timezone: Asia/Shanghai # 时区
|
||||
twofactor: true # 是否启用两步验证
|
||||
disqus: # 评论相关
|
||||
shortname: xxxxxx
|
||||
publickey: wdSgxRm9rdGAlLKFcFdToBe3GT4SibmV7Y8EjJQ0r4GWXeKtxpopMAeIeoI2dTEg
|
||||
|
||||
@@ -22,10 +22,10 @@
|
||||
<label for=password class="sr-only">密码</label>
|
||||
<input type=password id=password name=password class="text-l w-100" placeholder="密码">
|
||||
</p>
|
||||
<!-- <p>
|
||||
{{if .TwoFactor}}<p>
|
||||
<label for=code class="sr-only">两步验证</label>
|
||||
<input type=text id=code name=code class="text-l w-100" placeholder="两步验证">
|
||||
</p> -->
|
||||
</p>{{end}}
|
||||
<p class=submit>
|
||||
<button type=submit class="btn btn-l w-100 primary">登录</button>
|
||||
</p>
|
||||
|
||||
@@ -7,11 +7,37 @@
|
||||
<div class="row typecho-page-main">
|
||||
<div class="col-mb-12 col-tb-3">
|
||||
<p>
|
||||
<img class="profile-avatar" src="//{{$.Qiniu.Domain}}/static/img/avatar.png" alt="{{.Blogger.BlogName}}" />
|
||||
<img class="profile-avatar" src="//{{$.Qiniu.Domain}}/static/img/avatar.png" alt="{{.Blogger.BlogName}}" />
|
||||
</p>
|
||||
<h2>{{.Blogger.BlogName}}</h2>
|
||||
<p>{{.Blogger.SubTitle}}</p>
|
||||
<p>最后登录: {{dateformat .Account.LoginAt "2006/01/02 15:04"}}</p>
|
||||
{{if .Account.TwoFactorSecret}}
|
||||
<strong>2FA 已绑定 <a class="unbind-2fa" href="/admin/profile?unbind=true">解绑</a></strong>
|
||||
{{end}}
|
||||
{{if $.TwoFactorSecret}}
|
||||
<strong>2FA 绑定</strong>
|
||||
<form action="/admin/api/twofactor" method="post" enctype="application/x-www-form-urlencoded">
|
||||
<ul class="typecho-option">
|
||||
<li>
|
||||
<img src="{{$.TwoFactorSecret}}" alt="2fa code" style="width:180px;height:180px;">
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="typecho-option">
|
||||
<li>
|
||||
<input id="2fa-0-1" name="code" type="text" class="text" />
|
||||
<p class="description">
|
||||
输入 2FA 验证码, 用于确认绑定.</p>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="typecho-option typecho-option-submit">
|
||||
<li>
|
||||
<button type="submit" class="btn primary">
|
||||
确认绑定</button>
|
||||
</li>
|
||||
</ul>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="col-mb-12 col-tb-6 col-tb-offset-1 typecho-content-panel" role="form">
|
||||
<section>
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 客户端
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
1
go.mod
1
go.mod
@@ -7,6 +7,7 @@ require (
|
||||
github.com/gin-contrib/sessions v0.0.4
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/qiniu/go-sdk/v7 v7.11.0
|
||||
github.com/satori/go.uuid v1.2.0
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
|
||||
4
go.sum
4
go.sum
@@ -33,6 +33,8 @@ github.com/aws/aws-sdk-go v1.34.28 h1:sscPpn/Ns3i0F4HPEWAVcwdIRaZZCuL7llJ2/60yPI
|
||||
github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48=
|
||||
github.com/bkaradzic/go-lz4 v1.0.0/go.mod h1:0YdlkowM3VswSROI7qDxhRvJ3sLhlFrRRwjwegp5jy4=
|
||||
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
|
||||
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
|
||||
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||
@@ -280,6 +282,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/qiniu/dyn v1.3.0/go.mod h1:E8oERcm8TtwJiZvkQPbcAh0RL8jO1G0VXJMW3FAWdkk=
|
||||
github.com/qiniu/go-sdk/v7 v7.11.0 h1:Cdx/1E3ybv0OFKnkGwoDN/t6bCCntjrWhwWuRaqI3XQ=
|
||||
github.com/qiniu/go-sdk/v7 v7.11.0/go.mod h1:btsaOc8CA3hdVloULfFdDgDc+g4f3TDZEFsDY0BLE+w=
|
||||
|
||||
@@ -93,6 +93,8 @@ type General struct {
|
||||
Length int
|
||||
// 时区, 一般配置为 Asia/Shanghai
|
||||
Timezone string
|
||||
// 是否启用两步验证
|
||||
TwoFactor bool
|
||||
}
|
||||
|
||||
// Account 账户配置
|
||||
@@ -101,12 +103,6 @@ type Account struct {
|
||||
Username string
|
||||
// *必须配置, 后台登录密码。登录后请后台立即修改
|
||||
Password string
|
||||
// 邮箱, 用于显示
|
||||
Email string
|
||||
// 手机号, 用于显示
|
||||
PhoneNumber string
|
||||
// 地址, 用于显示
|
||||
Address string
|
||||
}
|
||||
|
||||
// Blogger 博客配置, 无需配置,程序默认初始化,可在后台更改
|
||||
|
||||
@@ -7,11 +7,12 @@ import "time"
|
||||
|
||||
// Account 博客账户
|
||||
type Account struct {
|
||||
Username string `gorm:"column:username;primaryKey" bson:"username"` // 用户名
|
||||
Password string `gorm:"column:password;not null" bson:"password"` // 密码
|
||||
Email string `gorm:"column:email;not null" bson:"email"` // 邮件地址
|
||||
PhoneN string `gorm:"column:phone_n;not null" bson:"phone_n"` // 手机号
|
||||
Address string `gorm:"column:address;not null" bson:"address"` // 地址信息
|
||||
Username string `gorm:"column:username;primaryKey" bson:"username"` // 用户名
|
||||
Password string `gorm:"column:password;not null" bson:"password"` // 密码
|
||||
Email string `gorm:"column:email;not null" bson:"email"` // 邮件地址
|
||||
PhoneN string `gorm:"column:phone_n;not null" bson:"phone_n"` // 手机号
|
||||
Address string `gorm:"column:address;not null" bson:"address"` // 地址信息
|
||||
TwoFactorSecret string `gorm:"column:two_factor_secret" bson:"two_factor_secret"` // 两步验证密钥
|
||||
|
||||
LogoutAt time.Time `gorm:"column:logout_at;not null" bson:"logout_at"` // 登出时间
|
||||
LoginIP string `gorm:"column:login_ip;not null" bson:"login_ip"` // 最近登录IP
|
||||
|
||||
Reference in New Issue
Block a user