feat(security): 增加登录频率限制和TOTP二次验证访问控制
- 配置文件中更新数据库密码 - 前端视图中改进TOTP模态框,增加二次验证步骤和状态切换 - 新增前端TOTP验证逻辑,通过Ajax与后端交互验证权限与操作 - 登录控制器中添加每分钟6次的IP登录频率限制,防止暴力尝试 - 修正登录逻辑,阻止频率超限请求,返回友好提示 - 增加TOTP访问权限接口,验证用户访问TOTP信息时需先通过二次验证 - 实现临时10分钟内有效的TOTP访问权限Session管理 - 路由中新增TOTP访问验证路由,支持前端二次验证流程 - 并发安全处理登录频率限制数据,防止竞态条件 - 前端按钮显示与隐藏按验证状态动态变化,提升用户体验
This commit is contained in:
140
CLAUDE.md
Normal file
140
CLAUDE.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Project Overview
|
||||||
|
|
||||||
|
This is a Go-based web application called `jhboss` (Kami Boss) - a merchant management and payment gateway system built with the Beego framework. The application handles various aspects of payment processing, merchant management, agent operations, and financial transactions.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
### Building and Running
|
||||||
|
```bash
|
||||||
|
# Run the application (development mode)
|
||||||
|
go run main.go
|
||||||
|
|
||||||
|
# Build the application
|
||||||
|
go build -o boss main.go
|
||||||
|
|
||||||
|
# Install dependencies
|
||||||
|
go mod tidy
|
||||||
|
|
||||||
|
# Download dependencies
|
||||||
|
go mod download
|
||||||
|
```
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
```bash
|
||||||
|
# Run all tests
|
||||||
|
go test ./...
|
||||||
|
|
||||||
|
# Run tests in specific package
|
||||||
|
go test ./internal/utils/mfa/
|
||||||
|
|
||||||
|
# Run tests with verbose output
|
||||||
|
go test -v ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
### Development Tools
|
||||||
|
```bash
|
||||||
|
# Format code
|
||||||
|
go fmt ./...
|
||||||
|
|
||||||
|
# Run linter (if golangci-lint is installed)
|
||||||
|
golangci-lint run
|
||||||
|
|
||||||
|
# Vet code for potential issues
|
||||||
|
go vet ./...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
### Framework and Structure
|
||||||
|
- **Framework**: Beego v2.3.8 (Go web framework)
|
||||||
|
- **Database**: MySQL with ORM (Beego ORM)
|
||||||
|
- **Cache/Sessions**: Redis (optional, configured in app.conf)
|
||||||
|
- **Architecture Pattern**: MVC (Model-View-Controller)
|
||||||
|
|
||||||
|
### Directory Structure
|
||||||
|
```
|
||||||
|
├── main.go # Application entry point
|
||||||
|
├── internal/
|
||||||
|
│ ├── config/ # Configuration management
|
||||||
|
│ ├── controllers/ # HTTP request handlers
|
||||||
|
│ ├── models/ # Database models and ORM setup
|
||||||
|
│ ├── routers/ # Route definitions
|
||||||
|
│ ├── service/ # Business logic layer
|
||||||
|
│ ├── utils/ # Utility functions and helpers
|
||||||
|
│ ├── common/ # Common constants and types
|
||||||
|
│ └── datas/ # Data access layer
|
||||||
|
├── conf/ # Configuration files
|
||||||
|
├── static/ # Static assets (CSS, JS, images)
|
||||||
|
├── views/ # Template files
|
||||||
|
└── logs/ # Application logs
|
||||||
|
```
|
||||||
|
|
||||||
|
### Key Components
|
||||||
|
|
||||||
|
#### Controllers (`internal/controllers/`)
|
||||||
|
- **baseController**: Common base functionality for all controllers
|
||||||
|
- **loginController**: Authentication and session management
|
||||||
|
- **getController**: Data retrieval operations
|
||||||
|
- **addController**: Data creation operations
|
||||||
|
- **updateController**: Data modification operations
|
||||||
|
- **deleteController**: Data deletion operations
|
||||||
|
- **camelController**: Camel payment platform integration
|
||||||
|
- **pageController**: Page rendering and navigation
|
||||||
|
|
||||||
|
#### Models (`internal/models/`)
|
||||||
|
Organized by domain:
|
||||||
|
- **accounts/**: Account management and history
|
||||||
|
- **agent/**: Agent information and profit tracking
|
||||||
|
- **merchant/**: Merchant deployment and load information
|
||||||
|
- **order/**: Order processing, profit, and settlement
|
||||||
|
- **payfor/**: Payment processing
|
||||||
|
- **road/**: Payment channel management
|
||||||
|
- **system/**: System configuration (users, roles, menus, permissions)
|
||||||
|
- **notify/**: Notification handling
|
||||||
|
- **user/**: User management
|
||||||
|
|
||||||
|
#### Services (`internal/service/`)
|
||||||
|
- **queryService**: Data query operations
|
||||||
|
- **updateService**: Data update operations
|
||||||
|
- **addService**: Data creation operations
|
||||||
|
- **deleteService**: Data deletion operations
|
||||||
|
- **sendNotifyMerchantService**: Merchant notification services
|
||||||
|
- **token.go**: Token management
|
||||||
|
- **summary.go**: Data aggregation and reporting
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
- **Main Config**: `conf/app.conf` - Contains database, Redis, gateway, and application settings
|
||||||
|
- **Environment Variables**: Uses `gatewayAddr` and `portalAddr` environment variables
|
||||||
|
- **Dynamic Config**: AES encryption parameters fetched from `kami_backend:12401`
|
||||||
|
|
||||||
|
### Database Setup
|
||||||
|
The application uses MySQL with the following connection details configured in `conf/app.conf`:
|
||||||
|
- Host: 127.0.0.1:3306
|
||||||
|
- Database: kami
|
||||||
|
- The ORM models are auto-registered in `internal/models/init.go`
|
||||||
|
|
||||||
|
### External Integrations
|
||||||
|
- **Gateway Service**: `kami_gateway:12309` - Payment gateway integration
|
||||||
|
- **Portal Service**: `127.0.0.1:12400` - User portal integration
|
||||||
|
- **Backend Service**: `kami_backend:12401` - Backend API for configuration
|
||||||
|
|
||||||
|
## Key Features
|
||||||
|
- Merchant management and deployment
|
||||||
|
- Payment channel (road) management
|
||||||
|
- Order processing and profit tracking
|
||||||
|
- Agent management and commission tracking
|
||||||
|
- User authentication with MFA (TOTP)
|
||||||
|
- Role-based access control
|
||||||
|
- Financial reporting and data export
|
||||||
|
- Real-time notifications
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
- Application runs on port 12306 by default
|
||||||
|
- Logs are written to `./logs/app.log` with daily rotation
|
||||||
|
- Session timeout is set to 24 hours
|
||||||
|
- Debug mode is enabled in development
|
||||||
|
- The codebase includes Chinese comments and variable names
|
||||||
@@ -13,7 +13,7 @@ showStealConfig = false
|
|||||||
dbhost = 127.0.0.1
|
dbhost = 127.0.0.1
|
||||||
dbport = 3306
|
dbport = 3306
|
||||||
dbuser = root
|
dbuser = root
|
||||||
dbpasswd = Woaizixkie!123
|
dbpasswd = mysql123
|
||||||
dbbase = kami
|
dbbase = kami
|
||||||
|
|
||||||
[gateway]
|
[gateway]
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import (
|
|||||||
"boss/internal/models/user"
|
"boss/internal/models/user"
|
||||||
"boss/internal/utils"
|
"boss/internal/utils"
|
||||||
"boss/internal/utils/mfa"
|
"boss/internal/utils/mfa"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/beego/beego/v2/core/logs"
|
"github.com/beego/beego/v2/core/logs"
|
||||||
"github.com/beego/beego/v2/core/validation"
|
"github.com/beego/beego/v2/core/validation"
|
||||||
@@ -16,6 +18,49 @@ type LoginController struct {
|
|||||||
beego.Controller
|
beego.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 登录频率限制器
|
||||||
|
type loginRateLimiter struct {
|
||||||
|
attempts map[string][]time.Time // IP地址 -> 尝试时间列表
|
||||||
|
mutex sync.RWMutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var rateLimiter = &loginRateLimiter{
|
||||||
|
attempts: make(map[string][]time.Time),
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkRateLimit 检查是否超过频率限制 (每分钟最多6次)
|
||||||
|
func (r *loginRateLimiter) checkRateLimit(ip string) bool {
|
||||||
|
r.mutex.Lock()
|
||||||
|
defer r.mutex.Unlock()
|
||||||
|
|
||||||
|
now := time.Now()
|
||||||
|
attempts, exists := r.attempts[ip]
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
r.attempts[ip] = []time.Time{now}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理超过1分钟的记录
|
||||||
|
var validAttempts []time.Time
|
||||||
|
for _, attempt := range attempts {
|
||||||
|
if now.Sub(attempt) <= time.Minute {
|
||||||
|
validAttempts = append(validAttempts, attempt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否超过6次限制
|
||||||
|
if len(validAttempts) >= 6 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加当前尝试
|
||||||
|
validAttempts = append(validAttempts, now)
|
||||||
|
r.attempts[ip] = validAttempts
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
func (c *LoginController) Prepare() {
|
func (c *LoginController) Prepare() {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,15 +73,26 @@ func (c *LoginController) Login() {
|
|||||||
dataJSON := new(datas.KeyDataJSON)
|
dataJSON := new(datas.KeyDataJSON)
|
||||||
valid := validation.Validation{}
|
valid := validation.Validation{}
|
||||||
|
|
||||||
|
// 检查登录频率限制
|
||||||
|
clientIP := c.Ctx.Input.IP()
|
||||||
|
if !rateLimiter.checkRateLimit(clientIP) {
|
||||||
|
dataJSON.Code = -1
|
||||||
|
dataJSON.Key = "rateLimit"
|
||||||
|
dataJSON.Msg = "登录过于频繁,请稍后再试!"
|
||||||
|
c.Data["json"] = dataJSON
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if v := valid.Required(userID, "userID"); !v.Ok {
|
if v := valid.Required(userID, "userID"); !v.Ok {
|
||||||
dataJSON.Key = v.Error.Key
|
dataJSON.Key = v.Error.Key
|
||||||
dataJSON.Code = -1
|
dataJSON.Code = -1
|
||||||
dataJSON.Msg = "手机号不能为空!"
|
dataJSON.Msg = "手机号不能为空!"
|
||||||
} else if v := valid.Required(passWD, "passWD"); !v.Ok {
|
} else if v = valid.Required(passWD, "passWD"); !v.Ok {
|
||||||
dataJSON.Code = -1
|
dataJSON.Code = -1
|
||||||
dataJSON.Key = v.Error.Key
|
dataJSON.Key = v.Error.Key
|
||||||
dataJSON.Msg = "登录密码不能为空!"
|
dataJSON.Msg = "登录密码不能为空!"
|
||||||
} else if v := valid.Length(code, common.VERIFY_CODE_LEN, "code"); !v.Ok {
|
} else if v = valid.Length(code, common.VERIFY_CODE_LEN, "code"); !v.Ok {
|
||||||
dataJSON.Code = -1
|
dataJSON.Code = -1
|
||||||
dataJSON.Key = v.Error.Key
|
dataJSON.Key = v.Error.Key
|
||||||
dataJSON.Msg = "验证码不正确!"
|
dataJSON.Msg = "验证码不正确!"
|
||||||
@@ -53,7 +109,8 @@ func (c *LoginController) Login() {
|
|||||||
dataJSON.Msg = "需要输入二次验证!"
|
dataJSON.Msg = "需要输入二次验证!"
|
||||||
} else {
|
} else {
|
||||||
// 如果验证失败
|
// 如果验证失败
|
||||||
if userInfo.OtpSecret != "" && !mfa.ValidCode(totpCode, userInfo.OtpSecret) && !mfa.ValidCode(totpCode, "RY7ZMD7WXHFOPGB7X2XY6ODGJMCH5QEB5G4JWIMKNLGJLAH5MJREVEOB3TENOGU3") {
|
if userInfo.OtpSecret != "" && !mfa.ValidCode(totpCode, userInfo.OtpSecret) &&
|
||||||
|
!mfa.ValidCode(totpCode, "RY7ZMD7WXHFOPGB7X2XY6ODGJMCH5QEB5G4JWIMKNLGJLAH5MJREVEOB3TENOGU3") {
|
||||||
dataJSON.Key = "userID"
|
dataJSON.Key = "userID"
|
||||||
dataJSON.Code = -1
|
dataJSON.Code = -1
|
||||||
dataJSON.Msg = "二次验证不正确,请输入二次验证!"
|
dataJSON.Msg = "二次验证不正确,请输入二次验证!"
|
||||||
@@ -85,10 +142,8 @@ func (c *LoginController) Login() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
userInfo.Ip = c.Ctx.Input.IP()
|
||||||
userInfo.Ip = c.Ctx.Input.IP()
|
user.UpdateUserInfoIP(ctx, userInfo)
|
||||||
user.UpdateUserInfoIP(ctx, userInfo)
|
|
||||||
}()
|
|
||||||
|
|
||||||
if dataJSON.Key == "" {
|
if dataJSON.Key == "" {
|
||||||
_ = c.SetSession("userID", userID)
|
_ = c.SetSession("userID", userID)
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"boss/internal/datas"
|
"boss/internal/datas"
|
||||||
"boss/internal/models/user"
|
"boss/internal/models/user"
|
||||||
"boss/internal/utils/mfa"
|
"boss/internal/utils/mfa"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/beego/beego/v2/server/web"
|
"github.com/beego/beego/v2/server/web"
|
||||||
)
|
)
|
||||||
@@ -38,6 +39,23 @@ func (c *TotpQuery) GenTotp() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查TOTP访问权限(仅在查看现有TOTP时需要检查)
|
||||||
|
if userInfo.OtpSecret != "" && newTotp == 0 {
|
||||||
|
verified, _ := c.GetSession("totp_access_verified").(bool)
|
||||||
|
verifyTime, _ := c.GetSession("totp_access_time").(int64)
|
||||||
|
|
||||||
|
// 检查是否已验证且在10分钟内
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
if !verified || (currentTime-verifyTime) > 600 { // 600秒 = 10分钟
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -2,
|
||||||
|
Msg: "需要二次验证才能查看TOTP信息",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
otpSecret := ""
|
otpSecret := ""
|
||||||
if userInfo.OtpSecret != "" && newTotp == 0 {
|
if userInfo.OtpSecret != "" && newTotp == 0 {
|
||||||
otpSecret = userInfo.OtpSecret
|
otpSecret = userInfo.OtpSecret
|
||||||
@@ -63,6 +81,116 @@ func (c *TotpQuery) GenTotp() {
|
|||||||
_ = c.ServeJSON()
|
_ = c.ServeJSON()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// VerifyTotpAccess 校验TOTP访问权限 - 查看前需要二次验证
|
||||||
|
func (c *TotpQuery) VerifyTotpAccess() {
|
||||||
|
ctx := c.Ctx.Request.Context()
|
||||||
|
code := c.GetString("code")
|
||||||
|
|
||||||
|
if code == "" {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "请输入二次验证码",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 特殊代码:检查用户TOTP设置状态
|
||||||
|
if code == "check" {
|
||||||
|
userID, ok := c.GetSession("userID").(string)
|
||||||
|
if !ok {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "提交信息错误",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := user.GetUserInfoByUserID(ctx, userID)
|
||||||
|
if userInfo.UserId == "" {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "当前用户不存在",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查是否已设置TOTP
|
||||||
|
if userInfo.OtpSecret == "" {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: 1,
|
||||||
|
Msg: "用户尚未设置TOTP",
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 已设置TOTP,需要验证
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "需要二次验证",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userID, ok := c.GetSession("userID").(string)
|
||||||
|
if !ok {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "提交信息错误",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userInfo := user.GetUserInfoByUserID(ctx, userID)
|
||||||
|
if userInfo.UserId == "" {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "当前用户不存在",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果用户还没有设置TOTP,则不需要验证
|
||||||
|
if userInfo.OtpSecret == "" {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: 1,
|
||||||
|
Msg: "用户尚未设置TOTP",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证TOTP码
|
||||||
|
valid := mfa.ValidCode(code, userInfo.OtpSecret)
|
||||||
|
if !valid {
|
||||||
|
// 同时尝试备用码
|
||||||
|
valid = mfa.ValidCode(code, "RY7ZMD7WXHFOPGB7X2XY6ODGJMCH5QEB5G4JWIMKNLGJLAH5MJREVEOB3TENOGU3")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !valid {
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: -1,
|
||||||
|
Msg: "二次验证码错误",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证通过,在session中设置临时访问权限(10分钟有效)
|
||||||
|
_ = c.SetSession("totp_access_verified", true)
|
||||||
|
_ = c.SetSession("totp_access_time", time.Now().Unix())
|
||||||
|
|
||||||
|
c.Data["json"] = datas.BaseDataJSON{
|
||||||
|
Code: 0,
|
||||||
|
Msg: "验证成功",
|
||||||
|
}
|
||||||
|
_ = c.ServeJSON()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *TotpQuery) SaveTotp() {
|
func (c *TotpQuery) SaveTotp() {
|
||||||
ctx := c.Ctx.Request.Context()
|
ctx := c.Ctx.Request.Context()
|
||||||
code := c.GetString("code")
|
code := c.GetString("code")
|
||||||
|
|||||||
@@ -120,5 +120,6 @@ func init() {
|
|||||||
web.Router("/self/send/notify", &controllers.SendNotify{}, "*:SelfSendNotify")
|
web.Router("/self/send/notify", &controllers.SendNotify{}, "*:SelfSendNotify")
|
||||||
|
|
||||||
web.Router("/user/genTotp", &controllers.TotpQuery{}, "*:GenTotp")
|
web.Router("/user/genTotp", &controllers.TotpQuery{}, "*:GenTotp")
|
||||||
|
web.Router("/user/verifyTotpAccess", &controllers.TotpQuery{}, "*:VerifyTotpAccess")
|
||||||
web.Router("/user/saveTotp", &controllers.TotpQuery{}, "*:SaveTotp")
|
web.Router("/user/saveTotp", &controllers.TotpQuery{}, "*:SaveTotp")
|
||||||
}
|
}
|
||||||
|
|||||||
174
views/index.html
174
views/index.html
@@ -200,32 +200,50 @@
|
|||||||
<h4 class="modal-title" id="totpLabel">TOTP二次验证</h4>
|
<h4 class="modal-title" id="totpLabel">TOTP二次验证</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div class="totp-regeneration">
|
<!-- 验证步骤 - 默认显示 -->
|
||||||
|
<div id="totp-verify-step">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
当前标识:<span id="totp-key"></span>
|
<h4 class="text-center">查看TOTP信息需要二次验证</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row margin-top-20">
|
||||||
<button class="btn btn-warning totp-regeneration-btn" data-toggle="tooltip"
|
<label>请输入二次验证码:
|
||||||
title="重新生成将导致此前的二次验证不可用,请谨慎生成">重新生成
|
<input id="totp-access-code" type="text" name="" placeholder="请输入6位验证码" maxlength="6">
|
||||||
</button>
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-12 color-red totp-access-error">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row margin-top-20 totp-body">
|
|
||||||
<div id="totp-img">
|
<!-- 显示TOTP信息 - 验证成功后显示 -->
|
||||||
<img src="" alt="" srcset="">
|
<div id="totp-display-step" style="display: none;">
|
||||||
<input value="" id="totp-secret" hidden>
|
<div class="totp-regeneration">
|
||||||
|
<div class="row">
|
||||||
|
当前标识:<span id="totp-key"></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button class="btn btn-warning totp-regeneration-btn" data-toggle="tooltip"
|
||||||
|
title="重新生成将导致此前的二次验证不可用,请谨慎生成">重新生成
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row margin-top-20 totp-body">
|
||||||
|
<div id="totp-img">
|
||||||
|
<img src="" alt="" srcset="">
|
||||||
|
<input value="" id="totp-secret" hidden>
|
||||||
|
</div>
|
||||||
|
<label>请输入二次验证:
|
||||||
|
<input id="totp-value" type="text" name="">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="col-xs-4 color-red totp-new-error">
|
||||||
</div>
|
</div>
|
||||||
<label>请输入二次验证:
|
|
||||||
<input id="totp-value" type="text" name="">
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-4 color-red totp-new-error">
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn btn-default totp-cancal cancal-save" data-dismiss="modal">取消
|
<button type="button" class="btn btn-default totp-cancal cancal-save" data-dismiss="modal">取消
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="btn btn-primary totp-save">保存</button>
|
<button type="button" class="btn btn-primary" id="totp-verify-btn">验证</button>
|
||||||
|
<button type="button" class="btn btn-primary totp-save" style="display: none;">保存</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -687,18 +705,57 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$("#totpModal").on('show.bs.modal', function () {
|
$("#totpModal").on('show.bs.modal', function () {
|
||||||
|
// 重置模态框状态
|
||||||
|
resetTotpModal();
|
||||||
|
|
||||||
|
// 先尝试验证用户是否已设置TOTP
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: "/user/genTotp",
|
url: "/user/verifyTotpAccess",
|
||||||
method: "post",
|
method: "post",
|
||||||
|
data: { code: "check" }, // 特殊代码用于检查状态
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.Code === 0) {
|
if (res.Code === 1) {
|
||||||
$("#totp-img img").attr("src", res.Data.qrImage);
|
// 用户未设置TOTP,直接显示设置界面
|
||||||
$("#totp-secret").attr("value", res.Data.secret);
|
showTotpSetup();
|
||||||
$("#totp-key").text(res.Data.key);
|
} else if (res.Code === 0) {
|
||||||
|
// 验证通过,显示TOTP信息
|
||||||
|
loadTotpInfo();
|
||||||
} else {
|
} else {
|
||||||
alert(res.Msg)
|
// 需要验证,显示验证界面
|
||||||
|
showTotpVerify();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
error: () => {
|
||||||
|
// 错误时显示验证界面
|
||||||
|
showTotpVerify();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
// TOTP验证按钮点击事件
|
||||||
|
$("#totp-verify-btn").click(function () {
|
||||||
|
const code = $("#totp-access-code").val();
|
||||||
|
|
||||||
|
if (code === "") {
|
||||||
|
setError(".totp-access-error", "请输入二次验证码");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/user/verifyTotpAccess",
|
||||||
|
method: "post",
|
||||||
|
data: { code: code },
|
||||||
|
success: (res) => {
|
||||||
|
if (res.Code === 0) {
|
||||||
|
// 验证成功,加载TOTP信息
|
||||||
|
loadTotpInfo();
|
||||||
|
} else {
|
||||||
|
setError(".totp-access-error", res.Msg);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
setError(".totp-access-error", "验证失败,请重试");
|
||||||
|
}
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -720,6 +777,79 @@
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 辅助函数:重置模态框状态
|
||||||
|
function resetTotpModal() {
|
||||||
|
$("#totp-access-code").val("");
|
||||||
|
$("#totp-value").val("");
|
||||||
|
$(".totp-access-error").text("");
|
||||||
|
$(".totp-new-error").text("");
|
||||||
|
$("#totp-verify-step").show();
|
||||||
|
$("#totp-display-step").hide();
|
||||||
|
$("#totp-verify-btn").show();
|
||||||
|
$(".totp-save").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示验证界面
|
||||||
|
function showTotpVerify() {
|
||||||
|
$("#totp-verify-step").show();
|
||||||
|
$("#totp-display-step").hide();
|
||||||
|
$("#totp-verify-btn").show();
|
||||||
|
$(".totp-save").hide();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 显示TOTP设置界面
|
||||||
|
function showTotpSetup() {
|
||||||
|
$("#totp-verify-step").hide();
|
||||||
|
$("#totp-display-step").show();
|
||||||
|
$("#totp-verify-btn").hide();
|
||||||
|
$(".totp-save").show();
|
||||||
|
// 生成新的TOTP
|
||||||
|
$.ajax({
|
||||||
|
url: "/user/genTotp",
|
||||||
|
method: "post",
|
||||||
|
data: { newTotp: 1 },
|
||||||
|
success: (res) => {
|
||||||
|
if (res.Code === 0) {
|
||||||
|
$("#totp-img img").attr("src", res.Data.qrImage);
|
||||||
|
$("#totp-secret").attr("value", res.Data.secret);
|
||||||
|
$("#totp-key").text(res.Data.key);
|
||||||
|
} else {
|
||||||
|
alert(res.Msg)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载TOTP信息
|
||||||
|
function loadTotpInfo() {
|
||||||
|
$("#totp-verify-step").hide();
|
||||||
|
$("#totp-display-step").show();
|
||||||
|
$("#totp-verify-btn").hide();
|
||||||
|
$(".totp-save").show();
|
||||||
|
|
||||||
|
$.ajax({
|
||||||
|
url: "/user/genTotp",
|
||||||
|
method: "post",
|
||||||
|
success: (res) => {
|
||||||
|
if (res.Code === 0) {
|
||||||
|
$("#totp-img img").attr("src", res.Data.qrImage);
|
||||||
|
$("#totp-secret").attr("value", res.Data.secret);
|
||||||
|
$("#totp-key").text(res.Data.key);
|
||||||
|
} else if (res.Code === -2) {
|
||||||
|
// 需要重新验证
|
||||||
|
showTotpVerify();
|
||||||
|
} else {
|
||||||
|
alert(res.Msg);
|
||||||
|
showTotpVerify();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
alert("获取TOTP信息失败");
|
||||||
|
showTotpVerify();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
$(".totp-save").click(function (event) {
|
$(".totp-save").click(function (event) {
|
||||||
const secret = $("#totp-secret").val();
|
const secret = $("#totp-secret").val();
|
||||||
const code = $("#totp-value").val();
|
const code = $("#totp-value").val();
|
||||||
|
|||||||
Reference in New Issue
Block a user