Skyroc Admin React
路由

权限

理解权限模型、静态权限、动态权限、直接 URL 授权、超级角色和 403

这一页解释 apps/admin 的权限是怎么判定的。先把“权限是什么”讲清楚,再看静态模式和动态模式两条数据来源,最后是 403 的来源和排查。

判定发生在守卫链路里,但守卫只负责“判定失败就跳 /403”。守卫的整体流程见 路由守卫

权限模型

先记住四条,后面所有细节都是它的展开:

维度当前模型
权限是什么角色码,例如 R_SUPERR_ADMIN。不是细粒度操作码,也不是接口权限。
怎么判定用户 roles 命中任意一个路由 permissions(OR 语义),或拥有超级角色,即通过。
作用域路由级。matched routes 上的 permissions 必须全部通过(matches.every),不只是叶子页面。
静态 vs 动态只是 permissions 这份数据的来源不同(前端 staticData / 后端路由节点),判定函数是同一套。

判定逻辑集中在 packages/web/admin-layouts/src/features/menus/permissions.ts,只有三个函数:

export function hasAnyRoutePermission(permissions, userInfo) {
  if (!permissions?.length) {
    return true;
  }

  const roles = userInfo?.roles ?? [];
  const { permissionSuperRole } = getAdminLayoutsOptions();

  if (permissionSuperRole && roles.includes(permissionSuperRole)) {
    return true;
  }

  return permissions.some(permission => roles.includes(permission));
}

export function hasRoutePermission(routeMeta, userInfo) {
  return hasAnyRoutePermission(routeMeta?.permissions, userInfo);
}

export function hasMatchedRoutePermission(matches, userInfo) {
  return matches.every(match => hasRoutePermission(match.staticData, userInfo));
}
路由配置用户角色结果
未配置 permissions任意允许。
配置 permissions命中任意一个权限允许。
配置 permissions包含超级角色允许。
配置 permissions都不命中403。

当前项目没有按钮级 / 接口级权限permissions 只控制路由能不能进入。如果未来需要更细粒度的权限,应在业务模块里单独设计,而不是复用这套路由级判定。

当前事实

事实当前实现
静态权限判断hasMatchedRoutePermission(matches, userInfo)
动态直达授权hasAuthorizedRoutePath(currentRoutePath, userInfo)
超级角色.envVITE_STATIC_SUPER_ROLE,当前为 R_SUPER
路由模式.envVITE_AUTH_ROUTE_MODE,当前默认为 static
注入超级角色bootstrap.tsxsetupAdminLayouts({ permissionSuperRole })

适用场景

场景重点位置
静态路由进入 403页面 staticData.permissions、用户 rolesVITE_STATIC_SUPER_ROLE
动态菜单模式直接输入 URL 进入 403后端路由树、quickReferenceMenushasAuthorizedRoutePath()
给某个页面加访问角色staticData.permissions(静态)或后端节点 permissions(动态)
某角色应能访问全部页面VITE_STATIC_SUPER_ROLE

静态权限

静态模式由 .env 控制:

VITE_AUTH_ROUTE_MODE=static

在静态模式下,守卫会检查当前 matched routes 上所有 staticData.permissions

if (import.meta.env.VITE_AUTH_ROUTE_MODE === 'static' && !hasMatchedRoutePermission(matches, userInfo)) {
  throw redirect({ to: '/403' });
}

hasMatchedRoutePermission() 会对每个 match 调用 hasRoutePermission(),规则就是上面的权限模型。

超级角色来自:

VITE_STATIC_SUPER_ROLE=R_SUPER

例如角色管理页当前需要超级角色:

staticData: {
  title: 'role',
  i18nKey: 'route.manage_role',
  menu: {
    icon: 'carbon:user-role',
    order: 2
  },
  permissions: ['R_SUPER']
}

如果父 layout route 和子页面都配置了 permissions,静态模式会要求 matched route 全部通过。也就是说,子页面有权限但父级无权限,仍然会进入 403。这正是 matches.every 的体现。

动态权限和直接 URL 授权

动态模式下,菜单和授权索引来自后端路由接口。bootstrap.tsx 配置:

setupAdminLayouts({
  loadDynamicRoutes: loadAdminDynamicRoutes,
  routeMode: globalConfig.routeMode
});

useMenus().initMenus() 在 dynamic 模式下会加载后端路由:

const routeData = await loadDynamicRoutes();
const { allMenus, home, quickReferenceMenus } = menuGenerator.generate({
  backendRoutes: routeData.routes,
  home: routeData.home,
  userInfo
});

守卫随后检查当前路由 path:

const currentRoutePath = getCurrentRoutePath(matches);

if (currentRoutePath && !hasAuthorizedRoutePath(currentRoutePath, userInfo)) {
  throw redirect({ to: '/403' });
}

hasAuthorizedRoutePath() 只有在 dynamic 模式下才真正检查:

if (routeMode !== 'dynamic') {
  return true;
}

const menu = getQuickReferenceMenuByPath(path);

return Boolean(menu && hasRoutePermission(menu, userInfo));

这解决的是动态菜单模式下的“直接输入 URL”问题:即使某个页面组件存在,只要后端没有把这个 path 返回到当前用户的授权菜单树中,用户直接访问也会被拦到 /403

动态模式必须满足的条件

条件原因
后端返回的 path 要和前端 route path 一致quickReferenceMenus 按规范化 path 查找当前路由。
后端节点要带正确的 permissions动态模式的直接 URL 授权读取后端节点权限。
前端仍然要有真实页面文件TanStack Router 只渲染前端 route tree 中存在的组件。
后端返回的 home 应是可访问 route path登录后首页会用动态 home 覆盖默认首页。

403 的来源

当前进入 /403 主要有两个来源:

来源触发条件
静态权限失败VITE_AUTH_ROUTE_MODE=static,matched routes 的 staticData.permissions 未通过。
动态授权失败VITE_AUTH_ROUTE_MODE=dynamic,当前 path 不在 quickReferenceMenus 中,或节点权限未通过。

用户信息初始化失败不会进 /403,而是清理认证后回 /login。未登录也不会进 /403,而是回 /login。这两类属于守卫流程,见 路由守卫

排查顺序

现象先检查
静态模式某页面 403页面和父 layout 的 staticData.permissions,用户 rolesVITE_STATIC_SUPER_ROLE
动态模式菜单里没有该页面后端路由树是否返回该 path,path 是否和前端规范化 path 一致。
动态模式菜单有但直接访问 403后端节点 permissions 是否和用户 roles 匹配。
动态模式点击菜单 404前端是否真的有对应 apps/admin/src/pages 页面和生成的 route tree。
超级角色仍然 403VITE_STATIC_SUPER_ROLE 是否和用户 roles 中的值完全一致,setupAdminLayouts({ permissionSuperRole }) 是否注入。

常见误区

误区正确理解
静态模式只检查最后一个页面的权限当前使用 matches.every,父级 layout route 的权限也会参与。
动态模式只要前端页面存在就能访问必须在后端授权路由树中存在当前 path。
超级角色只影响菜单显示超级角色在 hasAnyRoutePermission() 中统一生效,菜单生成和守卫都会使用。
permissions 是按钮权限这里是路由访问权限;按钮级或接口级权限应在业务模块里单独设计。
多个 permissions 需要同时满足是 OR 语义,命中任意一个即可通过。

相关位置

主题继续查看
登录拦截、用户初始化、外链、守卫链路路由守卫
staticData.permissions 字段写法路由元信息
页面文件、路由组和 route tree路由概览
启动时注入 permissionSuperRole启动流程

On this page