- 新增 ops 错误日志记录器(ops_error_logger.go) - 新增 ops 主处理器(ops_handler.go) - 新增告警管理处理器(ops_alerts_handler.go) - 新增仪表板处理器(ops_dashboard_handler.go) - 新增实时监控处理器(ops_realtime_handler.go) - 新增配置管理处理器(ops_settings_handler.go) - 新增 WebSocket 处理器(ops_ws_handler.go) - 扩展设置 DTO 支持 ops 配置 - 新增客户端请求 ID 中间件(client_request_id.go) - 新增 WebSocket 查询令牌认证中间件(ws_query_token_auth.go) - 更新管理员认证中间件支持 ops 路由 - 注册 handler 依赖注入
194 lines
5.2 KiB
Go
194 lines
5.2 KiB
Go
// Package middleware provides HTTP middleware for authentication, authorization, and request processing.
|
||
package middleware
|
||
|
||
import (
|
||
"crypto/subtle"
|
||
"errors"
|
||
"strings"
|
||
|
||
"github.com/Wei-Shaw/sub2api/internal/service"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
// NewAdminAuthMiddleware 创建管理员认证中间件
|
||
func NewAdminAuthMiddleware(
|
||
authService *service.AuthService,
|
||
userService *service.UserService,
|
||
settingService *service.SettingService,
|
||
) AdminAuthMiddleware {
|
||
return AdminAuthMiddleware(adminAuth(authService, userService, settingService))
|
||
}
|
||
|
||
// adminAuth 管理员认证中间件实现
|
||
// 支持两种认证方式(通过不同的 header 区分):
|
||
// 1. Admin API Key: x-api-key: <admin-api-key>
|
||
// 2. JWT Token: Authorization: Bearer <jwt-token> (需要管理员角色)
|
||
func adminAuth(
|
||
authService *service.AuthService,
|
||
userService *service.UserService,
|
||
settingService *service.SettingService,
|
||
) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
// WebSocket upgrade requests cannot set Authorization headers in browsers.
|
||
// For admin WebSocket endpoints (e.g. Ops realtime), allow passing the JWT via
|
||
// Sec-WebSocket-Protocol (subprotocol list) using a prefixed token item:
|
||
// Sec-WebSocket-Protocol: sub2api-admin, jwt.<token>
|
||
if isWebSocketUpgradeRequest(c) {
|
||
if token := extractJWTFromWebSocketSubprotocol(c); token != "" {
|
||
if !validateJWTForAdmin(c, token, authService, userService) {
|
||
return
|
||
}
|
||
c.Next()
|
||
return
|
||
}
|
||
}
|
||
|
||
// 检查 x-api-key header(Admin API Key 认证)
|
||
apiKey := c.GetHeader("x-api-key")
|
||
if apiKey != "" {
|
||
if !validateAdminAPIKey(c, apiKey, settingService, userService) {
|
||
return
|
||
}
|
||
c.Next()
|
||
return
|
||
}
|
||
|
||
// 检查 Authorization header(JWT 认证)
|
||
authHeader := c.GetHeader("Authorization")
|
||
if authHeader != "" {
|
||
parts := strings.SplitN(authHeader, " ", 2)
|
||
if len(parts) == 2 && parts[0] == "Bearer" {
|
||
if !validateJWTForAdmin(c, parts[1], authService, userService) {
|
||
return
|
||
}
|
||
c.Next()
|
||
return
|
||
}
|
||
}
|
||
|
||
// 无有效认证信息
|
||
AbortWithError(c, 401, "UNAUTHORIZED", "Authorization required")
|
||
}
|
||
}
|
||
|
||
func isWebSocketUpgradeRequest(c *gin.Context) bool {
|
||
if c == nil || c.Request == nil {
|
||
return false
|
||
}
|
||
// RFC6455 handshake uses:
|
||
// Connection: Upgrade
|
||
// Upgrade: websocket
|
||
upgrade := strings.ToLower(strings.TrimSpace(c.GetHeader("Upgrade")))
|
||
if upgrade != "websocket" {
|
||
return false
|
||
}
|
||
connection := strings.ToLower(c.GetHeader("Connection"))
|
||
return strings.Contains(connection, "upgrade")
|
||
}
|
||
|
||
func extractJWTFromWebSocketSubprotocol(c *gin.Context) string {
|
||
if c == nil {
|
||
return ""
|
||
}
|
||
raw := strings.TrimSpace(c.GetHeader("Sec-WebSocket-Protocol"))
|
||
if raw == "" {
|
||
return ""
|
||
}
|
||
|
||
// The header is a comma-separated list of tokens. We reserve the prefix "jwt."
|
||
// for carrying the admin JWT.
|
||
for _, part := range strings.Split(raw, ",") {
|
||
p := strings.TrimSpace(part)
|
||
if strings.HasPrefix(p, "jwt.") {
|
||
token := strings.TrimSpace(strings.TrimPrefix(p, "jwt."))
|
||
if token != "" {
|
||
return token
|
||
}
|
||
}
|
||
}
|
||
return ""
|
||
}
|
||
|
||
// validateAdminAPIKey 验证管理员 API Key
|
||
func validateAdminAPIKey(
|
||
c *gin.Context,
|
||
key string,
|
||
settingService *service.SettingService,
|
||
userService *service.UserService,
|
||
) bool {
|
||
storedKey, err := settingService.GetAdminAPIKey(c.Request.Context())
|
||
if err != nil {
|
||
AbortWithError(c, 500, "INTERNAL_ERROR", "Internal server error")
|
||
return false
|
||
}
|
||
|
||
// 未配置或不匹配,统一返回相同错误(避免信息泄露)
|
||
if storedKey == "" || subtle.ConstantTimeCompare([]byte(key), []byte(storedKey)) != 1 {
|
||
AbortWithError(c, 401, "INVALID_ADMIN_KEY", "Invalid admin API key")
|
||
return false
|
||
}
|
||
|
||
// 获取真实的管理员用户
|
||
admin, err := userService.GetFirstAdmin(c.Request.Context())
|
||
if err != nil {
|
||
AbortWithError(c, 500, "INTERNAL_ERROR", "No admin user found")
|
||
return false
|
||
}
|
||
|
||
c.Set(string(ContextKeyUser), AuthSubject{
|
||
UserID: admin.ID,
|
||
Concurrency: admin.Concurrency,
|
||
})
|
||
c.Set(string(ContextKeyUserRole), admin.Role)
|
||
c.Set("auth_method", "admin_api_key")
|
||
return true
|
||
}
|
||
|
||
// validateJWTForAdmin 验证 JWT 并检查管理员权限
|
||
func validateJWTForAdmin(
|
||
c *gin.Context,
|
||
token string,
|
||
authService *service.AuthService,
|
||
userService *service.UserService,
|
||
) bool {
|
||
// 验证 JWT token
|
||
claims, err := authService.ValidateToken(token)
|
||
if err != nil {
|
||
if errors.Is(err, service.ErrTokenExpired) {
|
||
AbortWithError(c, 401, "TOKEN_EXPIRED", "Token has expired")
|
||
return false
|
||
}
|
||
AbortWithError(c, 401, "INVALID_TOKEN", "Invalid token")
|
||
return false
|
||
}
|
||
|
||
// 从数据库获取用户
|
||
user, err := userService.GetByID(c.Request.Context(), claims.UserID)
|
||
if err != nil {
|
||
AbortWithError(c, 401, "USER_NOT_FOUND", "User not found")
|
||
return false
|
||
}
|
||
|
||
// 检查用户状态
|
||
if !user.IsActive() {
|
||
AbortWithError(c, 401, "USER_INACTIVE", "User account is not active")
|
||
return false
|
||
}
|
||
|
||
// 检查管理员权限
|
||
if !user.IsAdmin() {
|
||
AbortWithError(c, 403, "FORBIDDEN", "Admin access required")
|
||
return false
|
||
}
|
||
|
||
c.Set(string(ContextKeyUser), AuthSubject{
|
||
UserID: user.ID,
|
||
Concurrency: user.Concurrency,
|
||
})
|
||
c.Set(string(ContextKeyUserRole), user.Role)
|
||
c.Set("auth_method", "jwt")
|
||
|
||
return true
|
||
}
|