Skip to content

Commit d9bdf92

Browse files
committed
feat(auth): 添加TOTP双因素认证功能
- 实现MFA登录挑战和验证流程 - 添加TOTP启用、禁用和恢复码管理接口 - 集成前端MFA登录表单和验证界面 - 支持应急重置令牌配置 - 添加数据库迁移脚本和模型定义
1 parent 8a20301 commit d9bdf92

File tree

18 files changed

+2888
-302
lines changed

18 files changed

+2888
-302
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,7 @@ docker-compose up -d
146146
| [🌐 Host 管理](docs/features/host.md) | 域名映射、DNS 配置、测速持久化 |
147147
| [🤖 Telegram 机器人](docs/features/telegram-bot.md) | 命令列表、配置指南 |
148148
| [📜 脚本功能](docs/script_support.md) | 节点过滤、内容后处理、函数参考 |
149+
| [🔐 双重验证(MFA)](docs/features/mfa.md) | TOTP 设置、恢复码、应急重置流程 |
149150

150151
### 👨‍💻 开发者
151152

api/auth.go

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -131,47 +131,22 @@ func UserLogin(c *gin.Context) {
131131
}
132132
// 登录成功,清除失败记录
133133
limiter.ClearFailures(ip)
134-
// 生成token
135-
token, err := GetToken(user)
136-
if err != nil {
137-
utils.Error("获取token失败: %v", err)
138-
utils.FailWithMsg(c, "获取token失败")
139-
return
140-
}
141-
142-
// 异步发送登录通知
143-
go func(username, ip string) {
144-
location, err := geoip.GetLocation(ip)
134+
if user.TOTPEnabled {
135+
challengeToken, err := issuePendingMFAChallenge(user)
145136
if err != nil {
146-
location = "未知位置"
147-
}
148-
if location == "" {
149-
location = "未知位置"
150-
}
151-
timeStr := time.Now().Format("2006-01-02 15:04:05")
152-
153-
payload := notifications.Payload{
154-
Title: "用户登录通知",
155-
Message: fmt.Sprintf("用户 %s 已登录\nIP: %s (%s)\n时间: %s", username, ip, location, timeStr),
156-
Data: map[string]interface{}{
157-
"username": username,
158-
"ip": ip,
159-
"location": location,
160-
"time": timeStr,
161-
},
162-
Time: timeStr,
137+
utils.Error("生成 MFA 挑战失败: %v", err)
138+
utils.FailWithMsg(c, "生成登录验证失败")
139+
return
163140
}
141+
utils.OkDetailed(c, "需要进行二次验证", gin.H{
142+
"requiresMFA": true,
143+
"challengeToken": challengeToken,
144+
"methods": []string{"totp", "recovery_code"},
145+
})
146+
return
147+
}
164148

165-
notifications.Publish("security.user_login", payload)
166-
}(username, ip)
167-
168-
// 登录成功返回token
169-
utils.OkDetailed(c, "登录成功", gin.H{
170-
"accessToken": token,
171-
"tokenType": "Bearer",
172-
"refreshToken": nil,
173-
"expires": nil,
174-
})
149+
respondLoginSuccess(c, user, ip)
175150
}
176151

177152
// UserOut 用户退出登录
@@ -181,3 +156,28 @@ func UserOut(c *gin.Context) {
181156
utils.OkWithMsg(c, "退出成功")
182157
}
183158
}
159+
160+
func notifyUserLogin(username, ip string) {
161+
location, err := geoip.GetLocation(ip)
162+
if err != nil {
163+
location = "未知位置"
164+
}
165+
if location == "" {
166+
location = "未知位置"
167+
}
168+
timeStr := time.Now().Format("2006-01-02 15:04:05")
169+
170+
payload := notifications.Payload{
171+
Title: "用户登录通知",
172+
Message: fmt.Sprintf("用户 %s 已登录\nIP: %s (%s)\n时间: %s", username, ip, location, timeStr),
173+
Data: map[string]interface{}{
174+
"username": username,
175+
"ip": ip,
176+
"location": location,
177+
"time": timeStr,
178+
},
179+
Time: timeStr,
180+
}
181+
182+
notifications.Publish("security.user_login", payload)
183+
}

0 commit comments

Comments
 (0)