| 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 |
- // 路由角色白名单中间件(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:角色不在白名单
- }
- }
|