* 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
595 lines
13 KiB
Go
595 lines
13 KiB
Go
package controller
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"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-gonic/gin"
|
|
)
|
|
|
|
// Setup2FARequest 设置2FA请求结构
|
|
type Setup2FARequest struct {
|
|
Code string `json:"code" binding:"required"`
|
|
}
|
|
|
|
// Verify2FARequest 验证2FA请求结构
|
|
type Verify2FARequest struct {
|
|
Code string `json:"code" binding:"required"`
|
|
FlowToken string `json:"flow_token,omitempty"`
|
|
}
|
|
|
|
type twoFALoginFlowPayload struct {
|
|
AuthVersion int64 `json:"auth_version"`
|
|
}
|
|
|
|
// Setup2FAResponse 设置2FA响应结构
|
|
type Setup2FAResponse struct {
|
|
Secret string `json:"secret"`
|
|
QRCodeData string `json:"qr_code_data"`
|
|
BackupCodes []string `json:"backup_codes"`
|
|
}
|
|
|
|
// Setup2FA 初始化2FA设置
|
|
func Setup2FA(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
|
|
// 检查用户是否已经启用2FA
|
|
existing, err := model.GetTwoFAByUserId(userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if existing != nil && existing.IsEnabled {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "用户已启用2FA,请先禁用后重新设置",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 如果存在已禁用的2FA记录,先删除它
|
|
if existing != nil && !existing.IsEnabled {
|
|
if err := existing.DeletePendingTwoFASetup(); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
existing = nil // 重置为nil,后续将创建新记录
|
|
}
|
|
|
|
// 获取用户信息
|
|
user, err := model.GetUserById(userId, false)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
|
|
// 生成TOTP密钥
|
|
key, err := common.GenerateTOTPSecret(user.Username)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "生成2FA密钥失败",
|
|
})
|
|
common.SysLog("生成TOTP密钥失败: " + err.Error())
|
|
return
|
|
}
|
|
|
|
// 生成备用码
|
|
backupCodes, err := common.GenerateBackupCodes()
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "生成备用码失败",
|
|
})
|
|
common.SysLog("生成备用码失败: " + err.Error())
|
|
return
|
|
}
|
|
|
|
// 生成二维码数据
|
|
qrCodeData := common.GenerateQRCodeData(key.Secret(), user.Username)
|
|
|
|
// 创建或更新2FA记录(暂未启用)
|
|
twoFA := &model.TwoFA{
|
|
UserId: userId,
|
|
Secret: key.Secret(),
|
|
IsEnabled: false,
|
|
}
|
|
|
|
if err := twoFA.CreatePendingTwoFASetup(); err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
|
|
// 创建备用码记录
|
|
if err := model.CreatePendingTwoFASetupBackupCodes(userId, backupCodes); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "保存备用码失败",
|
|
})
|
|
common.SysLog("保存备用码失败: " + err.Error())
|
|
return
|
|
}
|
|
|
|
// 记录操作日志
|
|
model.RecordLog(userId, model.LogTypeSystem, "开始设置两步验证")
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "2FA设置初始化成功,请使用认证器扫描二维码并输入验证码完成设置",
|
|
"data": Setup2FAResponse{
|
|
Secret: key.Secret(),
|
|
QRCodeData: qrCodeData,
|
|
BackupCodes: backupCodes,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Enable2FA 启用2FA
|
|
func Enable2FA(c *gin.Context) {
|
|
var req Setup2FARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "参数错误",
|
|
})
|
|
return
|
|
}
|
|
|
|
userId := c.GetInt("id")
|
|
|
|
// 获取2FA记录
|
|
twoFA, err := model.GetTwoFAByUserId(userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if twoFA == nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "请先完成2FA初始化设置",
|
|
})
|
|
return
|
|
}
|
|
if twoFA.IsEnabled {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "2FA已经启用",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 验证TOTP验证码
|
|
cleanCode, err := common.ValidateNumericCode(req.Code)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
if !common.ValidateTOTPCode(twoFA.Secret, cleanCode) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "验证码或备用码错误,请重试",
|
|
})
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 记录操作日志
|
|
model.RecordLog(userId, model.LogTypeSystem, "成功启用两步验证")
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "两步验证启用成功",
|
|
"data": authRotationData(bundle),
|
|
})
|
|
}
|
|
|
|
// Disable2FA 禁用2FA
|
|
func Disable2FA(c *gin.Context) {
|
|
var req Verify2FARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "参数错误",
|
|
})
|
|
return
|
|
}
|
|
|
|
userId := c.GetInt("id")
|
|
|
|
// 获取2FA记录
|
|
twoFA, err := model.GetTwoFAByUserId(userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if twoFA == nil || !twoFA.IsEnabled {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "用户未启用2FA",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 验证TOTP验证码或备用码
|
|
cleanCode, err := common.ValidateNumericCode(req.Code)
|
|
isValidTOTP := false
|
|
isValidBackup := false
|
|
|
|
if err == nil {
|
|
// 尝试验证TOTP
|
|
isValidTOTP, _ = twoFA.ValidateTOTPAndUpdateUsage(cleanCode)
|
|
}
|
|
|
|
if !isValidTOTP {
|
|
// 尝试验证备用码
|
|
isValidBackup, err = twoFA.ValidateBackupCodeAndUpdateUsage(req.Code)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
if !isValidTOTP && !isValidBackup {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "验证码或备用码错误,请重试",
|
|
})
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
// 记录操作日志
|
|
model.RecordLog(userId, model.LogTypeSystem, "禁用两步验证")
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "两步验证已禁用",
|
|
"data": authRotationData(bundle),
|
|
})
|
|
}
|
|
|
|
// Get2FAStatus 获取用户2FA状态
|
|
func Get2FAStatus(c *gin.Context) {
|
|
userId := c.GetInt("id")
|
|
|
|
twoFA, err := model.GetTwoFAByUserId(userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
|
|
status := map[string]interface{}{
|
|
"enabled": false,
|
|
"locked": false,
|
|
}
|
|
|
|
if twoFA != nil {
|
|
status["enabled"] = twoFA.IsEnabled
|
|
status["locked"] = twoFA.IsLocked()
|
|
if twoFA.IsEnabled {
|
|
// 获取剩余备用码数量
|
|
backupCount, err := model.GetUnusedBackupCodeCount(userId)
|
|
if err != nil {
|
|
common.SysLog("获取备用码数量失败: " + err.Error())
|
|
} else {
|
|
status["backup_codes_remaining"] = backupCount
|
|
}
|
|
}
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": status,
|
|
})
|
|
}
|
|
|
|
// RegenerateBackupCodes 重新生成备用码
|
|
func RegenerateBackupCodes(c *gin.Context) {
|
|
var req Verify2FARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "参数错误",
|
|
})
|
|
return
|
|
}
|
|
|
|
userId := c.GetInt("id")
|
|
|
|
// 获取2FA记录
|
|
twoFA, err := model.GetTwoFAByUserId(userId)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if twoFA == nil || !twoFA.IsEnabled {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "用户未启用2FA",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 验证TOTP验证码
|
|
cleanCode, err := common.ValidateNumericCode(req.Code)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
|
|
valid, err := twoFA.ValidateTOTPAndUpdateUsage(cleanCode)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
if !valid {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "验证码或备用码错误,请重试",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 生成新的备用码
|
|
backupCodes, err := common.GenerateBackupCodes()
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "生成备用码失败",
|
|
})
|
|
common.SysLog("生成备用码失败: " + err.Error())
|
|
return
|
|
}
|
|
|
|
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": "保存备用码失败",
|
|
})
|
|
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": data,
|
|
})
|
|
}
|
|
|
|
// Verify2FALogin 登录时验证2FA
|
|
func Verify2FALogin(c *gin.Context) {
|
|
var req Verify2FARequest
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "参数错误",
|
|
})
|
|
return
|
|
}
|
|
|
|
flow, err := model.GetAuthFlow(req.FlowToken, model.AuthFlowMatch{Purpose: model.AuthFlowPurposeTwoFALogin})
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "会话已过期,请重新登录",
|
|
})
|
|
return
|
|
}
|
|
// 获取用户信息
|
|
user, err := model.GetUserById(flow.UserId, false)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "用户不存在",
|
|
})
|
|
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)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
if twoFA == nil || !twoFA.IsEnabled {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "用户未启用2FA",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 验证TOTP验证码或备用码
|
|
cleanCode, err := common.ValidateNumericCode(req.Code)
|
|
isValidTOTP := false
|
|
isValidBackup := false
|
|
|
|
if err == nil {
|
|
// 尝试验证TOTP
|
|
isValidTOTP, _ = twoFA.ValidateTOTPAndUpdateUsage(cleanCode)
|
|
}
|
|
|
|
if !isValidTOTP {
|
|
// 尝试验证备用码
|
|
isValidBackup, err = twoFA.ValidateBackupCodeAndUpdateUsage(req.Code)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": err.Error(),
|
|
})
|
|
return
|
|
}
|
|
}
|
|
|
|
if !isValidTOTP && !isValidBackup {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "验证码或备用码错误,请重试",
|
|
})
|
|
return
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
setupLoginAtAuthVersion(user, flowPayload.AuthVersion, c)
|
|
}
|
|
|
|
// Admin2FAStats 管理员获取2FA统计信息
|
|
func Admin2FAStats(c *gin.Context) {
|
|
stats, err := model.GetTwoFAStats()
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "",
|
|
"data": stats,
|
|
})
|
|
}
|
|
|
|
// AdminDisable2FA 管理员强制禁用用户2FA
|
|
func AdminDisable2FA(c *gin.Context) {
|
|
userIdStr := c.Param("id")
|
|
userId, err := strconv.Atoi(userIdStr)
|
|
if err != nil {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "用户ID格式错误",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 检查目标用户权限
|
|
targetUser, err := model.GetUserById(userId, false)
|
|
if err != nil {
|
|
common.ApiError(c, err)
|
|
return
|
|
}
|
|
|
|
myRole := c.GetInt("role")
|
|
if !canManageTargetRole(myRole, targetUser.Role) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "无权操作同级或更高级用户的2FA设置",
|
|
})
|
|
return
|
|
}
|
|
|
|
// 禁用2FA
|
|
if err := model.DisableTwoFAWithAuthVersion(userId); err != nil {
|
|
if errors.Is(err, model.ErrTwoFANotEnabled) {
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": false,
|
|
"message": "用户未启用2FA",
|
|
})
|
|
return
|
|
}
|
|
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)
|
|
|
|
c.JSON(http.StatusOK, gin.H{
|
|
"success": true,
|
|
"message": "用户2FA已被强制禁用",
|
|
})
|
|
}
|