refactor(auth): replace dashboard sessions with stateless tokens and session control (#6329)

* refactor(auth): replace dashboard sessions with stateless tokens

* feat(auth): harden session issuance and distributed enforcement

* fix(proxy): preserve trusted proxy compatibility defaults

* refactor: address dashboard auth review feedback

* refactor: remove classic frontend and flatten web app
This commit is contained in:
Calcium-Ion
2026-07-20 16:48:43 +08:00
committed by GitHub
parent 5a6c53d496
commit 31d70fca39
1605 changed files with 17511 additions and 147913 deletions
+86 -42
View File
@@ -6,9 +6,10 @@ import (
"strconv"
"github.com/QuantumNous/new-api/common"
"github.com/QuantumNous/new-api/middleware"
"github.com/QuantumNous/new-api/model"
"github.com/QuantumNous/new-api/service"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
)
@@ -19,7 +20,12 @@ type Setup2FARequest struct {
// Verify2FARequest 验证2FA请求结构
type Verify2FARequest struct {
Code string `json:"code" binding:"required"`
Code string `json:"code" binding:"required"`
FlowToken string `json:"flow_token,omitempty"`
}
type twoFALoginFlowPayload struct {
AuthVersion int64 `json:"auth_version"`
}
// Setup2FAResponse 设置2FA响应结构
@@ -49,7 +55,7 @@ func Setup2FA(c *gin.Context) {
// 如果存在已禁用的2FA记录,先删除它
if existing != nil && !existing.IsEnabled {
if err := existing.Delete(); err != nil {
if err := existing.DeletePendingTwoFASetup(); err != nil {
common.ApiError(c, err)
return
}
@@ -95,22 +101,13 @@ func Setup2FA(c *gin.Context) {
IsEnabled: false,
}
if existing != nil {
// 更新现有记录
twoFA.Id = existing.Id
err = twoFA.Update()
} else {
// 创建新记录
err = twoFA.Create()
}
if err != nil {
if err := twoFA.CreatePendingTwoFASetup(); err != nil {
common.ApiError(c, err)
return
}
// 创建备用码记录
if err := model.CreateBackupCodes(userId, backupCodes); err != nil {
if err := model.CreatePendingTwoFASetupBackupCodes(userId, backupCodes); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "保存备用码失败",
@@ -185,8 +182,18 @@ func Enable2FA(c *gin.Context) {
return
}
// 启用2FA
if err := twoFA.Enable(); err != nil {
identity, ok := middleware.GetSessionAuthIdentity(c)
if !ok {
common.ApiError(c, errors.New("当前认证方式不支持安全验证"))
return
}
// 启用2FA并原子推进用户鉴权版本
if err := twoFA.EnableWithAuthVersion(); err != nil {
common.ApiError(c, err)
return
}
bundle, err := service.AdvanceCurrentSessionToUserVersion(identity, "twofa_enabled")
if err != nil {
common.ApiError(c, err)
return
}
@@ -197,6 +204,7 @@ func Enable2FA(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "两步验证启用成功",
"data": authRotationData(bundle),
})
}
@@ -257,8 +265,18 @@ func Disable2FA(c *gin.Context) {
return
}
// 禁用2FA
if err := model.DisableTwoFA(userId); err != nil {
identity, ok := middleware.GetSessionAuthIdentity(c)
if !ok {
common.ApiError(c, errors.New("当前认证方式不支持安全验证"))
return
}
// 禁用2FA并原子推进用户鉴权版本
if err := model.DisableTwoFAWithAuthVersion(userId); err != nil {
common.ApiError(c, err)
return
}
bundle, err := service.AdvanceCurrentSessionToUserVersion(identity, "twofa_disabled")
if err != nil {
common.ApiError(c, err)
return
}
@@ -269,6 +287,7 @@ func Disable2FA(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "两步验证已禁用",
"data": authRotationData(bundle),
})
}
@@ -372,8 +391,13 @@ func RegenerateBackupCodes(c *gin.Context) {
return
}
// 保存新的备用码
if err := model.CreateBackupCodes(userId, backupCodes); err != nil {
identity, ok := middleware.GetSessionAuthIdentity(c)
if !ok {
common.ApiError(c, errors.New("当前认证方式不支持安全验证"))
return
}
// 保存新的备用码并原子推进用户鉴权版本
if err := model.ReplaceBackupCodesWithAuthVersion(userId, backupCodes); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "保存备用码失败",
@@ -381,16 +405,21 @@ func RegenerateBackupCodes(c *gin.Context) {
common.SysLog("保存备用码失败: " + err.Error())
return
}
bundle, err := service.AdvanceCurrentSessionToUserVersion(identity, "twofa_backup_codes_regenerated")
if err != nil {
common.ApiError(c, err)
return
}
// 记录操作日志
model.RecordLog(userId, model.LogTypeSystem, "重新生成两步验证备用码")
data := authRotationData(bundle)
data["backup_codes"] = backupCodes
c.JSON(http.StatusOK, gin.H{
"success": true,
"message": "备用码重新生成成功",
"data": map[string]interface{}{
"backup_codes": backupCodes,
},
"data": data,
})
}
@@ -405,26 +434,16 @@ func Verify2FALogin(c *gin.Context) {
return
}
// 从会话中获取pending用户信息
session := sessions.Default(c)
pendingUserId := session.Get("pending_user_id")
if pendingUserId == nil {
flow, err := model.GetAuthFlow(req.FlowToken, model.AuthFlowMatch{Purpose: model.AuthFlowPurposeTwoFALogin})
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "会话已过期,请重新登录",
})
return
}
userId, ok := pendingUserId.(int)
if !ok {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "会话数据无效,请重新登录",
})
return
}
// 获取用户信息
user, err := model.GetUserById(userId, false)
user, err := model.GetUserById(flow.UserId, false)
if err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -432,6 +451,21 @@ func Verify2FALogin(c *gin.Context) {
})
return
}
if user.Status != common.UserStatusEnabled {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "用户已被禁用",
})
return
}
var flowPayload twoFALoginFlowPayload
if err := common.UnmarshalJsonStr(flow.Payload, &flowPayload); err != nil || flowPayload.AuthVersion <= 0 || flowPayload.AuthVersion != user.AuthVersion {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "会话已过期,请重新登录",
})
return
}
// 获取2FA记录
twoFA, err := model.GetTwoFAByUserId(user.Id)
@@ -477,12 +511,18 @@ func Verify2FALogin(c *gin.Context) {
return
}
// 2FA验证成功,清理pending会话信息并完成登录
session.Delete("pending_username")
session.Delete("pending_user_id")
session.Save()
if _, err := model.ConsumeAuthFlow(req.FlowToken, model.AuthFlowMatch{
Purpose: model.AuthFlowPurposeTwoFALogin,
UserId: user.Id,
}); err != nil {
c.JSON(http.StatusOK, gin.H{
"success": false,
"message": "会话已过期,请重新登录",
})
return
}
setupLogin(user, c)
setupLoginAtAuthVersion(user, flowPayload.AuthVersion, c)
}
// Admin2FAStats 管理员获取2FA统计信息
@@ -529,7 +569,7 @@ func AdminDisable2FA(c *gin.Context) {
}
// 禁用2FA
if err := model.DisableTwoFA(userId); err != nil {
if err := model.DisableTwoFAWithAuthVersion(userId); err != nil {
if errors.Is(err, model.ErrTwoFANotEnabled) {
c.JSON(http.StatusOK, gin.H{
"success": false,
@@ -540,6 +580,10 @@ func AdminDisable2FA(c *gin.Context) {
common.ApiError(c, err)
return
}
if _, err := model.RevokeAllUserSessions(userId, "admin_twofa_disabled"); err != nil {
common.ApiError(c, err)
return
}
recordManageAuditFor(c, userId, "user.2fa_disable", nil)