Go 多租户中间件设计模式:14个中间件的流水线架构

306 阅读4分钟

标签: Go | 中间件 | chi | 多租户 | SaaS


前言

多租户SaaS的请求处理比单租户复杂得多:JWT认证、RBAC权限、租户隔离、配额检查、审计日志……

龙讯POS V2用14个中间件组成请求处理流水线,本文拆解每个中间件的实现。


一、中间件流水线

Request
  → RealIP          1. 获取真实IP(反向代理后)
  → Recoverer       2. Panic恢复(防崩溃)
  → Logger          3. 请求日志(含租户/用户信息)
  → Metrics         4. Prometheus指标
  → CORS            5. 跨域处理
  → SecurityHeaders 6. 安全头(X-Frame-Options等)
  → MaxBodySize     7. 请求体大小限制(2MB)
  → RateLimit       8. 全局限流(30 req/s)
  → JWTAuth         9. JWT认证(RS256验证)
  → RBAC           10. 角色权限检查
  → TenantMiddleware 11. 租户上下文注入
  → AuditMiddleware 12. 审计日志记录
  → QuotaCheck     13. 配额检查(门店/菜品/订单)
  → TenantRateLimit 14. 租户级限流(200 req/min)
  → Handler        业务处理

二、JWT认证中间件

func JWTAuth(rdb *redis.Client, publicKey *rsa.PublicKey) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            authHeader := r.Header.Get("Authorization")
            if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") {
                writeUnauthorized(w, "未提供认证令牌")
                return
            }

            tokenStr := authHeader[7:]
            claims, err := parseToken(tokenStr, publicKey)
            if err != nil {
                writeUnauthorized(w, "认证令牌无效")
                return
            }

            // 检查Token黑名单(注销后加入)
            blacklistKey := fmt.Sprintf("token:blacklist:%s", claims.ID)
            exists, _ := rdb.Exists(r.Context(), blacklistKey).Result()
            if exists > 0 {
                writeUnauthorized(w, "认证令牌已失效")
                return
            }

            // 注入上下文
            ctx := context.WithValue(r.Context(), claimsKey, claims)
            next.ServeHTTP(w, r.WithContext(ctx))
        })
    }
}

三、RBAC权限中间件

func RequireRole(roles ...string) func(http.Handler) http.Handler {
    roleSet := make(map[string]bool)
    for _, r := range roles {
        roleSet[r] = true
    }

    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            claims := GetClaims(r)
            if roleSet[claims.Role] {
                next.ServeHTTP(w, r)
                return
            }
            writeForbidden(w, "权限不足")
        })
    }
}

// 4级角色继承
func RequirePlatformAdmin() func(http.Handler) http.Handler {
    return RequireRole("platform_admin")
}

func RequireMerchantAdmin() func(http.Handler) http.Handler {
    return RequireRole("platform_admin", "merchant_admin")
}

func RequireStoreAdmin() func(http.Handler) http.Handler {
    return RequireRole("platform_admin", "merchant_admin", "store_admin")
}

func RequireStaff() func(http.Handler) http.Handler {
    return RequireRole("platform_admin", "merchant_admin", "store_admin", "staff")
}

四、租户上下文中间件

func TenantMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        claims := GetClaims(r)

        // 检查商户状态
        tenant := tenantRepo.FindByID(claims.TenantID)
        if tenant == nil {
            writeForbidden(w, "商户不存在")
            return
        }
        if tenant.Status == "frozen" || tenant.Status == "closed" {
            writeJSON(w, 403, model.Error(40101, "商户已冻结"))
            return
        }

        // 注入租户ID和门店ID到上下文
        ctx := context.WithValue(r.Context(), tenantIDKey, claims.TenantID)
        ctx = context.WithValue(ctx, storeIDKey, claims.StoreID)
        ctx = context.WithValue(ctx, userIDKey, claims.UserID)

        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

五、配额检查中间件

func QuotaCheck(db *gorm.DB, resource string) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            tid := GetTenantID(r)
            var tenant model.Tenant
            db.First(&tenant, tid)

            switch resource {
            case "store":
                var count int64
                db.Model(&model.Store{}).Where("tenant_id = ?", tid).Count(&count)
                if count >= tenant.MaxStores {
                    writeJSON(w, 422, model.Error(40004, "门店数已达上限"))
                    return
                }
            case "dish":
                var count int64
                db.Model(&model.Dish{}).Where("tenant_id = ?", tid).Count(&count)
                if count >= tenant.MaxDishes {
                    writeJSON(w, 422, model.Error(40004, "菜品数已达上限"))
                    return
                }
            case "order":
                var count int64
                today := time.Now().Format("2006-01-02")
                db.Model(&model.Order{}).Where("tenant_id = ? AND DATE(created_at) = ?", tid, today).Count(&count)
                if count >= tenant.MaxOrdersPerDay {
                    writeJSON(w, 422, model.Error(40004, "今日订单数已达上限"))
                    return
                }
            }

            next.ServeHTTP(w, r)
        })
    }
}

六、审计日志中间件

func AuditMiddleware(auditSvc *AuditService) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            // 只记录写操作
            if r.Method == "GET" || r.Method == "OPTIONS" {
                next.ServeHTTP(w, r)
                return
            }

            // 读取请求体
            body, _ := io.ReadAll(r.Body)
            r.Body = io.NopCloser(bytes.NewBuffer(body)) // 重新放回

            // 脱敏
            sanitized := sanitizeAuditData(string(body))

            // 获取before_data(handler手动设置)
            beforeData := r.Context().Value(auditBeforeDataKey)

            // 异步记录审计日志
            claims := GetClaims(r)
            go auditSvc.Log(
                context.Background(),
                claims.TenantID,
                claims.UserID,
                r.Method,
                r.URL.Path,
                0, // recordID(handler层无法获取)
                beforeData,
                sanitized,
                extractClientIP(r),
            )

            next.ServeHTTP(w, r)
        })
    }
}

七、限流中间件

// 全局限流
func RateLimit(rdb *redis.Client, limit int, window time.Duration) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            ip := extractClientIP(r)
            key := fmt.Sprintf("ratelimit:global:%s", ip)

            count, _ := rdb.Incr(r.Context(), key).Result()
            if count == 1 {
                rdb.Expire(r.Context(), key, window)
            }
            if count > int64(limit) {
                writeJSON(w, 429, model.Error(429, "请求过于频繁"))
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

// 租户级限流
func TenantRateLimit(rdb *redis.Client, limit int, window time.Duration) func(http.Handler) http.Handler {
    return func(next http.Handler) http.Handler {
        return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
            tid := GetTenantID(r)
            key := fmt.Sprintf("ratelimit:tenant:%d", tid)

            count, _ := rdb.Incr(r.Context(), key).Result()
            if count == 1 {
                rdb.Expire(r.Context(), key, window)
            }
            if count > int64(limit) {
                writeJSON(w, 429, model.Error(42901, "租户请求过于频繁"))
                return
            }

            next.ServeHTTP(w, r)
        })
    }
}

八、路由注册:中间件组合

// router.go — 中间件与路由的组合

// 公开路由(无限流、无认证)
r.Group(func(r chi.Router) {
    r.Use(middleware.RateLimit(rdb, 5, time.Minute)) // 仅限流
    r.Post("/api/v2/auth/login", authH.Login)
    r.Post("/api/v2/tenants/register", tenantH.Register)
})

// 认证路由(JWT + RBAC)
r.Group(func(r chi.Router) {
    r.Use(middleware.RateLimit(rdb, 30, time.Second))
    r.Use(middleware.JWTAuth(rdb, publicKey))
    r.Use(middleware.TenantMiddleware)

    // store_admin+ 可访问
    r.Group(func(r chi.Router) {
        r.Use(middleware.RequireStoreAdmin())
        r.Get("/api/v2/dishes", dishH.List)
        r.Post("/api/v2/orders", orderH.Create)
    })

    // merchant_admin+ 可访问
    r.Group(func(r chi.Router) {
        r.Use(middleware.RequireMerchantAdmin())
        r.Use(middleware.AuditMiddleware(auditSvc))
        r.Post("/api/v2/stores", storeH.Create)
        r.Use(middleware.QuotaCheck(db, "store"))
    })

    // platform_admin 可访问
    r.Group(func(r chi.Router) {
        r.Use(middleware.RequirePlatformAdmin())
        r.Get("/api/v2/platform/tenants", platformH.ListTenants)
        r.Get("/api/v2/platform/audit-logs", platformH.ListAuditLogs)
    })
})

九、总结

中间件职责关键技术
JWTAuth认证RS256 + Redis黑名单
RBAC授权4级角色继承
TenantMiddleware租户上下文context注入+状态检查
QuotaCheck配额数据库计数+套餐限制
AuditMiddleware审计哈希链+脱敏+异步写入
RateLimit限流Redis INCR+滑动窗口
TenantRateLimit租户限流租户级计数
MustChangePwd强制改密JWT Claims检查

14个中间件,各司其职,组合成完整的请求处理流水线。