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:
@@ -1,133 +1,60 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
"strings"
|
||||
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/QuantumNous/new-api/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
const (
|
||||
// SecureVerificationSessionKey 安全验证的 session key(与 controller 保持一致)
|
||||
SecureVerificationSessionKey = "secure_verified_at"
|
||||
secureVerificationMethodSessionKey = "secure_verified_method"
|
||||
// SecureVerificationTimeout 验证有效期(秒)
|
||||
SecureVerificationTimeout = 300 // 5分钟
|
||||
)
|
||||
|
||||
// SecureVerificationRequired 安全验证中间件
|
||||
// 检查用户是否在有效时间内通过了安全验证
|
||||
// 如果未验证或验证已过期,返回 401 错误
|
||||
// SecureVerificationRequired protects channel key disclosure. Other sensitive
|
||||
// operations validate their narrower proof scopes in their controller.
|
||||
func SecureVerificationRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 检查用户是否已登录
|
||||
userId := c.GetInt("id")
|
||||
if userId == 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"success": false,
|
||||
"message": "未登录",
|
||||
})
|
||||
c.Abort()
|
||||
if !RequireSecurityProof(c, "channel.key.read", []string{"2fa", "passkey"}) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查 session 中的验证时间戳
|
||||
session := sessions.Default(c)
|
||||
verifiedAtRaw := session.Get(SecureVerificationSessionKey)
|
||||
|
||||
if verifiedAtRaw == nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "需要安全验证",
|
||||
"code": "VERIFICATION_REQUIRED",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
verifiedAt, ok := verifiedAtRaw.(int64)
|
||||
if !ok {
|
||||
// session 数据格式错误
|
||||
clearSecureVerificationSession(session)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "验证状态异常,请重新验证",
|
||||
"code": "VERIFICATION_INVALID",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查验证是否过期
|
||||
elapsed := time.Now().Unix() - verifiedAt
|
||||
if elapsed >= SecureVerificationTimeout {
|
||||
// 验证已过期,清除 session
|
||||
clearSecureVerificationSession(session)
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": "验证已过期,请重新验证",
|
||||
"code": "VERIFICATION_EXPIRED",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func clearSecureVerificationSession(session sessions.Session) {
|
||||
session.Delete(SecureVerificationSessionKey)
|
||||
session.Delete(secureVerificationMethodSessionKey)
|
||||
_ = session.Save()
|
||||
}
|
||||
|
||||
// OptionalSecureVerification 可选的安全验证中间件
|
||||
// 如果用户已验证,则在 context 中设置标记,但不阻止请求继续
|
||||
// 用于某些需要区分是否已验证的场景
|
||||
func OptionalSecureVerification() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userId := c.GetInt("id")
|
||||
if userId == 0 {
|
||||
c.Set("secure_verified", false)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
session := sessions.Default(c)
|
||||
verifiedAtRaw := session.Get(SecureVerificationSessionKey)
|
||||
|
||||
if verifiedAtRaw == nil {
|
||||
c.Set("secure_verified", false)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
verifiedAt, ok := verifiedAtRaw.(int64)
|
||||
if !ok {
|
||||
c.Set("secure_verified", false)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
elapsed := time.Now().Unix() - verifiedAt
|
||||
if elapsed >= SecureVerificationTimeout {
|
||||
clearSecureVerificationSession(session)
|
||||
c.Set("secure_verified", false)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
c.Set("secure_verified", true)
|
||||
c.Set("secure_verified_at", verifiedAt)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// ClearSecureVerification 清除安全验证状态
|
||||
// 用于用户登出或需要强制重新验证的场景
|
||||
func ClearSecureVerification(c *gin.Context) {
|
||||
session := sessions.Default(c)
|
||||
clearSecureVerificationSession(session)
|
||||
// RequireSecurityProof validates a proof against the authenticated dashboard
|
||||
// session and writes the shared proof error contract on failure.
|
||||
func RequireSecurityProof(c *gin.Context, requiredScope string, allowedMethods []string) bool {
|
||||
identity, ok := GetSessionAuthIdentity(c)
|
||||
if !ok {
|
||||
securityProofError(c, "SECURITY_PROOF_INVALID", "安全验证状态无效")
|
||||
return false
|
||||
}
|
||||
raw := strings.TrimSpace(c.GetHeader("X-Security-Proof"))
|
||||
if raw == "" {
|
||||
securityProofError(c, "SECURITY_PROOF_REQUIRED", "需要安全验证")
|
||||
return false
|
||||
}
|
||||
if _, err := service.VerifySecurityProof(raw, identity, requiredScope, allowedMethods); err != nil {
|
||||
switch {
|
||||
case errors.Is(err, service.ErrAuthTokenExpired):
|
||||
securityProofError(c, "SECURITY_PROOF_EXPIRED", "安全验证已过期")
|
||||
case errors.Is(err, service.ErrProofScope):
|
||||
securityProofError(c, "SECURITY_PROOF_SCOPE_MISMATCH", "安全验证范围不匹配")
|
||||
case errors.Is(err, service.ErrProofMethod):
|
||||
securityProofError(c, "SECURITY_PROOF_METHOD_MISMATCH", "安全验证方式不匹配")
|
||||
default:
|
||||
securityProofError(c, "SECURITY_PROOF_INVALID", "安全验证状态无效")
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func securityProofError(c *gin.Context, code, message string) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"success": false,
|
||||
"message": message,
|
||||
"code": code,
|
||||
})
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user