// 路由角色白名单中间件(T6.5-d,RBAC 三层模型的"接口强制层",2026-11-06 用户拍板)。 // // 定位(RBAC 三层模型第②层,与展示层/数据层分工): // - 展示层(org/rbac.Resolver):role→菜单树/权限码,供 /auth/issue·/auth/me 下发; // - **本中间件(接口强制层)**:受保护路由声明允许访问的**角色白名单**,请求角色不在 // 白名单内 → code 1003(无权限);超管(super_admin)代码层全量放行,不查白名单; // - 数据层(org/service.assertInScope + Repository Scoping):随业务域,本中间件不涉。 // // 为什么是"角色白名单"而非"权限点判定"(拍板 #1 依据): // - 粒度=7 角色(super_admin/academic/finance/teacher/student/parent/market),与 // membership.role 对齐,路由声明直观(如 GET /org/institutions 只允许 academic+super_admin); // - 权限点(域.资源.动作)走展示层下发 + 前端 v-perm,接口强制用"角色"已足够—— // 避免每次请求查 t_role_permission 矩阵(TTL 缓存只覆盖菜单,路由白名单是静态声明); // - 白名单在路由注册处**声明式**挂上(org.Use(authMw, handler.RequireRole("academic"))), // 改权限=改路由声明(随域模块演进),不动中间件代码。 // // 数据流(与 T4-5 验签中间件衔接,须在其**之后**挂上): // 1. authMw 验签 + scope.Set(c, sc)(scope.Scope 已写入 c.Locals); // 2. 本中间件:scope.FromFiber(c) 取 *Scope; // 3. sc==nil → 1001(中间件链配置错误:白名单挂在公开路由上); // 4. sc.SuperAdmin → 放行(超管代码层全量,不查白名单); // 5. sc.Role ∈ 白名单 → c.Next();否则 1003(无权限,不泄漏"哪些角色可访问")。 // // 错误码: // - 1001(ErrUnauth):无已验签 scope(白名单误挂公开路由 / 验签中间件未先执行); // - 1003(ErrForbidden):角色不在白名单(已登录但无该接口权限)。 package handler import ( "github.com/gofiber/fiber/v2" apperrors "yuansiwei/unified-backend/internal/infra/errors" "yuansiwei/unified-backend/internal/infra/response" "yuansiwei/unified-backend/internal/infra/scope" ) // RequireRole 构造"路由角色白名单"中间件(受保护路由专用,须挂在验签中间件之后)。 // // 入参:roles []string:允许访问该路由的角色码白名单(≥1 个;空切片视为配置错误—— // 见 fail-fast:返回固定 1003 不放行,杜绝"空白名单=全放行"的灾难性误配)。 // 出参:fiber.Handler——可直接 app.Group(...).Use(authMw, RequireRole("academic","finance"))。 // // 语义: // - 超管(sc.SuperAdmin=true)恒放行(代码层全量,拍板:超管不受白名单约束); // - 普通角色:sc.Role 命中白名单任一角色才放行,否则 1003; // - sc==nil:1001(白名单误挂公开路由,纵深防御 fail-closed,绝不静默放行)。 // // 使用样例(main.go 路由挂载): // // org := app.Group("/org") // org.Use(authMw) // 先验签+scope 注入 // org.Use(handler.RequireRole("academic", "finance")) // 再角色白名单 // org.Get("/institutions", orgH.Institutions) func RequireRole(roles ...string) fiber.Handler { // 空白名单 fail-closed:拒绝所有(含超管除外,超管恒放行)。 // 配置"空白名单"通常是想放行全部角色——但正确做法是显式列全 7 角色或不挂本中间件; // 静默放行会让"忘了填白名单"的开发者以为"没配=不限制",实际限制的是空集,语义反直觉。 return func(c *fiber.Ctx) error { sc := scope.FromFiber(c) if sc == nil { return response.Fail(c, apperrors.ErrUnauth) // 1001:无 scope(白名单误挂公开路由) } if sc.SuperAdmin { return c.Next() // 超管代码层全量放行(不查白名单) } for _, r := range roles { if r == sc.Role { return c.Next() } } return response.Fail(c, apperrors.ErrForbidden) // 1003:角色不在白名单 } }