package middleware import ( "errors" "log" "net/http" "sort" "strings" "sync" "server/pkg/jwtutil" "server/services" beego "github.com/beego/beego/v2/server/web" "github.com/beego/beego/v2/server/web/context" ) // 模块套餐鉴权(PlanAuth) // // 把「接口前缀」映射到「功能模块编码」(yz_system_modules.code), // 当前租户生效套餐(含已开通的单品订单)未包含该模块时拒绝访问。 // // 与 JWT 鉴权一致支持三档模式(app.conf: plan_enforce): // - off :完全不启用 // - warn:观察模式,只记录「本会被拦截」的请求(默认,便于上线前核对映射) // - on :真正拦截,未开通返回 403 // // 映射表(app.conf: plan_module_paths)示例: // plan_module_paths = /backend/crm:crm,/backend/erp:erp,/backend/oa:oa // 按最长前缀匹配;未出现在映射表中的接口不做模块校验(默认放行), // 因此可以按模块逐个灰度开启,不会误伤用户/角色/文件/日志等通用接口。 const planMappingSeparator = ":" var ( errPlanNoToken = errors.New("未提供认证信息") errPlanBadToken = errors.New("认证信息格式错误") planOnce sync.Once planMode = AuthModeWarn planRoutes []planRoute ) // planRoute 接口前缀 → 模块编码 type planRoute struct { prefix string module string } func planInit() { planOnce.Do(func() { if v, err := beego.AppConfig.String("plan_enforce"); err == nil { switch strings.ToLower(strings.TrimSpace(v)) { case AuthModeOff, AuthModeWarn, AuthModeOn: planMode = strings.ToLower(strings.TrimSpace(v)) } } raw, _ := beego.AppConfig.String("plan_module_paths") for _, item := range strings.Split(raw, ",") { item = strings.TrimSpace(item) if item == "" { continue } parts := strings.SplitN(item, planMappingSeparator, 2) if len(parts) != 2 { log.Printf("[plan] 忽略无效映射(应为 前缀:模块编码): %s", item) continue } prefix := strings.TrimSpace(parts[0]) module := services.NormalizeModuleCode(parts[1]) if prefix == "" || module == "" { log.Printf("[plan] 忽略无效映射(前缀或模块编码为空): %s", item) continue } planRoutes = append(planRoutes, planRoute{prefix: prefix, module: module}) } // 最长前缀优先:/backend/crm/bidding 必须先于 /backend/crm 命中 sort.Slice(planRoutes, func(i, j int) bool { return len(planRoutes[i].prefix) > len(planRoutes[j].prefix) }) log.Printf("[plan] 模块套餐鉴权模式=%s,接口映射=%d 条", planMode, len(planRoutes)) }) } // planModuleOf 返回路径归属的模块编码;未配置归属时返回空字符串 func planModuleOf(path string) string { for _, r := range planRoutes { if strings.HasPrefix(path, r.prefix) { return r.module } } return "" } // PlanAuthMiddleware 模块套餐鉴权中间件 func PlanAuthMiddleware() beego.FilterFunc { return func(ctx *context.Context) { planInit() if planMode == AuthModeOff || len(planRoutes) == 0 { return } if ctx.Input.Method() == http.MethodOptions { return } path := cleanPath(ctx.Request.RequestURI) if !isProtected(path) || isWhitelisted(path) { return } module := planModuleOf(path) if module == "" { return } claims, err := planClaims(ctx) if err != nil { // 未登录 / 令牌无效:交给 JWTAuthMiddleware 与各控制器按原逻辑处理 return } if claims.TenantId <= 0 { return } if services.IsModuleEnabled(uint64(claims.TenantId), module) { return } reason := "模块未开通: " + module if planMode == AuthModeWarn { warnLog(path, reason) return } ctx.Output.SetStatus(http.StatusForbidden) _ = ctx.Output.JSON(map[string]interface{}{ "code": 403, "msg": "该功能未开通,请先购买对应套餐", }, false, false) } } // planClaims 解析 Authorization 头中的 JWT(仅用于本中间件的模块归属判断) func planClaims(ctx *context.Context) (*jwtutil.Claims, error) { authHeader := ctx.Request.Header.Get("Authorization") if authHeader == "" { return nil, errPlanNoToken } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { return nil, errPlanBadToken } return jwtutil.ParseToken(parts[1]) }