Files
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

142 lines
5.2 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package middleware
import (
"context"
"fmt"
"github.com/Wei-Shaw/sub2api/internal/service"
"github.com/gin-gonic/gin"
)
// StepUpAuthMiddleware 敏感操作 step-up 2FA 门控中间件类型。
type StepUpAuthMiddleware gin.HandlerFunc
// stepUpGrantChecker 抽象 TOTP step-up 授权检查能力(由 TotpService 实现)。
type stepUpGrantChecker interface {
HasStepUpGrant(ctx context.Context, userID int64, sessionKey string) (bool, error)
}
// stepUpUserReader 抽象用户读取能力(检查 TOTP 是否启用)。
type stepUpUserReader interface {
GetByID(ctx context.Context, id int64) (*service.User, error)
}
// stepUpSettingReader 抽象 step-up 功能开关读取能力(由 SettingService 实现)。
type stepUpSettingReader interface {
IsStepUpEnabled(ctx context.Context) bool
}
// StepUpSessionKey 计算 step-up 授权的会话键:
// 优先绑定当前会话(refresh token family),无会话 ID 的旧 token 退化为用户级键。
func StepUpSessionKey(c *gin.Context, userID int64) string {
if sid := c.GetString(ContextKeySessionID); sid != "" {
return sid
}
return fmt.Sprintf("u%d", userID)
}
// NewStepUpAuthMiddleware 创建敏感操作 step-up 2FA 门控中间件。
//
// 功能开关 step_up_enabled(默认关闭)关闭时中间件直接放行,行为与门控引入前一致。
// 开启时的通过条件(全部满足):
// 1. 必须是 JWT 认证的真人会话——admin API key(机器凭证)一律拒绝
// 2. 当前用户已启用 TOTP(未启用则拒绝并提示先启用 2FA)
// 3. 当前会话在有效期内完成过 TOTP step-up 验证(POST /api/v1/user/totp/step-up
//
// 失败响应使用可区分的错误码,前端据此弹出 TOTP 验证对话框后重试。
func NewStepUpAuthMiddleware(
totpService *service.TotpService,
userService *service.UserService,
settingService *service.SettingService,
) StepUpAuthMiddleware {
return StepUpAuthMiddleware(stepUpAuth(totpService, userService, stepUpSettingsOrNil(settingService)))
}
// stepUpSettingsOrNil 将可能为 nil 的具体指针归一化为接口,
// 避免 typed-nil 装箱后绕过 enforceStepUp 内的 nil 判断。
func stepUpSettingsOrNil(settingService *service.SettingService) stepUpSettingReader {
if settingService == nil {
return nil
}
return settingService
}
func stepUpAuth(grantChecker stepUpGrantChecker, userReader stepUpUserReader, settings stepUpSettingReader) gin.HandlerFunc {
return func(c *gin.Context) {
if !enforceStepUp(c, grantChecker, userReader, settings) {
return
}
c.Next()
}
}
// EnforceStepUp 对当前请求执行与 StepUpAuthMiddleware 相同语义的 step-up 门控,
// 供 handler 在需要按请求内容条件触发时调用(如仅当把用户角色提升为管理员时)。
// 校验失败时写入错误响应并中止请求,返回 false;通过返回 true。
func EnforceStepUp(
c *gin.Context,
totpService *service.TotpService,
userService *service.UserService,
settingService *service.SettingService,
) bool {
return enforceStepUp(c, totpService, userService, stepUpSettingsOrNil(settingService))
}
// EnforceStepUpAlways 与 EnforceStepUp 语义相同但不读取功能开关,无条件执行门控。
// 供调用方已确知门控必须生效的场景使用(如"关闭 step-up 开关"本身:调用方刚从
// 持久化设置读到开关为开启状态,不应依赖二次读取——读取失败会导致门控被跳过)。
func EnforceStepUpAlways(
c *gin.Context,
totpService *service.TotpService,
userService *service.UserService,
) bool {
return enforceStepUp(c, totpService, userService, nil)
}
func enforceStepUp(c *gin.Context, grantChecker stepUpGrantChecker, userReader stepUpUserReader, settings stepUpSettingReader) bool {
// 功能开关关闭时直接放行(含 admin API key),恢复门控引入前的行为。
// settings 为 nil 时保持门控(fail-closed):正常装配不会出现 nil。
if settings != nil && !settings.IsStepUpEnabled(c.Request.Context()) {
return true
}
if c.GetString("auth_method") == service.AuditAuthMethodAdminAPIKey {
AbortWithError(c, 403, "STEP_UP_ADMIN_API_KEY_FORBIDDEN",
"Admin API key cannot access this endpoint; a two-factor verified admin session is required")
return false
}
subject, ok := GetAuthSubjectFromContext(c)
if !ok || subject.UserID <= 0 {
AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
return false
}
user, err := userReader.GetByID(c.Request.Context(), subject.UserID)
if err != nil {
AbortWithError(c, 500, "INTERNAL_ERROR", "Failed to load user")
return false
}
if !user.TotpEnabled {
AbortWithError(c, 403, "STEP_UP_TOTP_NOT_ENABLED",
"This operation requires two-factor authentication; please enable TOTP first")
return false
}
sessionKey := StepUpSessionKey(c, subject.UserID)
granted, err := grantChecker.HasStepUpGrant(c.Request.Context(), subject.UserID, sessionKey)
if err != nil {
// 安全门控故障时选择 fail-closed。
AbortWithError(c, 503, "STEP_UP_UNAVAILABLE", "Step-up verification service unavailable")
return false
}
if !granted {
AbortWithError(c, 403, "STEP_UP_REQUIRED",
"This operation requires recent two-factor verification")
return false
}
return true
}