From 74b11c4c70bf6edbb980bb78a9fe20191b2262cf Mon Sep 17 00:00:00 2001 From: danial Date: Mon, 24 Nov 2025 22:39:12 +0800 Subject: [PATCH] =?UTF-8?q?feat(security):=20=E5=A2=9E=E5=8A=A0=E7=99=BB?= =?UTF-8?q?=E5=BD=95=E9=A2=91=E7=8E=87=E9=99=90=E5=88=B6=E5=92=8CTOTP?= =?UTF-8?q?=E4=BA=8C=E6=AC=A1=E9=AA=8C=E8=AF=81=E8=AE=BF=E9=97=AE=E6=8E=A7?= =?UTF-8?q?=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 配置文件中更新数据库密码 - 前端视图中改进TOTP模态框,增加二次验证步骤和状态切换 - 新增前端TOTP验证逻辑,通过Ajax与后端交互验证权限与操作 - 登录控制器中添加每分钟6次的IP登录频率限制,防止暴力尝试 - 修正登录逻辑,阻止频率超限请求,返回友好提示 - 增加TOTP访问权限接口,验证用户访问TOTP信息时需先通过二次验证 - 实现临时10分钟内有效的TOTP访问权限Session管理 - 路由中新增TOTP访问验证路由,支持前端二次验证流程 - 并发安全处理登录频率限制数据,防止竞态条件 - 前端按钮显示与隐藏按验证状态动态变化,提升用户体验 --- CLAUDE.md | 140 +++++++++++++++++++ conf/app.conf | 2 +- internal/controllers/loginController.go | 69 +++++++++- internal/controllers/totpController.go | 128 +++++++++++++++++ internal/routers/router.go | 1 + views/index.html | 174 +++++++++++++++++++++--- 6 files changed, 484 insertions(+), 30 deletions(-) create mode 100644 CLAUDE.md diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..749cecf --- /dev/null +++ b/CLAUDE.md @@ -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 \ No newline at end of file diff --git a/conf/app.conf b/conf/app.conf index 1987353..749a4b5 100644 --- a/conf/app.conf +++ b/conf/app.conf @@ -13,7 +13,7 @@ showStealConfig = false dbhost = 127.0.0.1 dbport = 3306 dbuser = root -dbpasswd = Woaizixkie!123 +dbpasswd = mysql123 dbbase = kami [gateway] diff --git a/internal/controllers/loginController.go b/internal/controllers/loginController.go index 8a0f7b3..1e2ac9d 100644 --- a/internal/controllers/loginController.go +++ b/internal/controllers/loginController.go @@ -6,6 +6,8 @@ import ( "boss/internal/models/user" "boss/internal/utils" "boss/internal/utils/mfa" + "sync" + "time" "github.com/beego/beego/v2/core/logs" "github.com/beego/beego/v2/core/validation" @@ -16,6 +18,49 @@ type LoginController struct { 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() { } @@ -28,15 +73,26 @@ func (c *LoginController) Login() { dataJSON := new(datas.KeyDataJSON) 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 { dataJSON.Key = v.Error.Key dataJSON.Code = -1 dataJSON.Msg = "手机号不能为空!" - } else if v := valid.Required(passWD, "passWD"); !v.Ok { + } else if v = valid.Required(passWD, "passWD"); !v.Ok { dataJSON.Code = -1 dataJSON.Key = v.Error.Key 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.Key = v.Error.Key dataJSON.Msg = "验证码不正确!" @@ -53,7 +109,8 @@ func (c *LoginController) Login() { dataJSON.Msg = "需要输入二次验证!" } 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.Code = -1 dataJSON.Msg = "二次验证不正确,请输入二次验证!" @@ -85,10 +142,8 @@ func (c *LoginController) Login() { } } - go func() { - userInfo.Ip = c.Ctx.Input.IP() - user.UpdateUserInfoIP(ctx, userInfo) - }() + userInfo.Ip = c.Ctx.Input.IP() + user.UpdateUserInfoIP(ctx, userInfo) if dataJSON.Key == "" { _ = c.SetSession("userID", userID) diff --git a/internal/controllers/totpController.go b/internal/controllers/totpController.go index 09fa2d2..c20a6ec 100644 --- a/internal/controllers/totpController.go +++ b/internal/controllers/totpController.go @@ -4,6 +4,7 @@ import ( "boss/internal/datas" "boss/internal/models/user" "boss/internal/utils/mfa" + "time" "github.com/beego/beego/v2/server/web" ) @@ -38,6 +39,23 @@ func (c *TotpQuery) GenTotp() { 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 := "" if userInfo.OtpSecret != "" && newTotp == 0 { otpSecret = userInfo.OtpSecret @@ -63,6 +81,116 @@ func (c *TotpQuery) GenTotp() { _ = 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() { ctx := c.Ctx.Request.Context() code := c.GetString("code") diff --git a/internal/routers/router.go b/internal/routers/router.go index a51d80b..1a7882c 100644 --- a/internal/routers/router.go +++ b/internal/routers/router.go @@ -120,5 +120,6 @@ func init() { web.Router("/self/send/notify", &controllers.SendNotify{}, "*:SelfSendNotify") web.Router("/user/genTotp", &controllers.TotpQuery{}, "*:GenTotp") + web.Router("/user/verifyTotpAccess", &controllers.TotpQuery{}, "*:VerifyTotpAccess") web.Router("/user/saveTotp", &controllers.TotpQuery{}, "*:SaveTotp") } diff --git a/views/index.html b/views/index.html index db0ea4c..8e221ec 100644 --- a/views/index.html +++ b/views/index.html @@ -200,32 +200,50 @@