156 lines
4.2 KiB
Go
156 lines
4.2 KiB
Go
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])
|
||
}
|