前端中级实战:基于 React 与 TypeScript 构建可维护的权限路由与菜单系统
做后台管理系统时,权限路由和菜单系统几乎是绕不过去的一关。刚开始项目小、角色少时,大家常常直接在页面里写几个 if (role === 'admin'),看起来挺快;但一旦页面增多、角色细分、菜单嵌套、按钮权限接进来,代码很快就会变成“谁也不敢动”的区域。
我自己在中型后台项目里踩过不少坑:
- 路由配置和菜单配置写了两份,后面必然不同步
- 页面能访问,但菜单不显示;或者菜单显示了,点进去 403
- 类型约束不够,后端一改字段,前端静悄悄出错
- 权限判断散落在组件里,后面做审计和排查非常痛苦
这篇文章从架构设计角度,带你做一套可维护、可扩展、类型安全的权限路由与菜单系统。技术栈使用 React + TypeScript + React Router,重点不只是“能跑”,而是“后面还能改”。
背景与问题
先看几个常见场景:
-
同一路由,不同角色看到不同菜单
- 管理员可见“用户管理”
- 普通运营只能看到“内容审核”
-
菜单来自统一配置,但页面访问要二次校验
- 菜单隐藏不等于安全
- 用户手输 URL 仍然可能访问页面
-
权限不只有角色
- 有的是角色型权限:
admin、editor - 有的是资源型权限:
user.read、user.edit - 有的是按钮级权限:
audit:approve
- 有的是角色型权限:
-
后端返回用户权限,前端要动态生成可访问路由
- 登录后根据当前用户权限裁剪路由树
- 页面刷新后能恢复
- 菜单和路由保持同源
这些问题背后,本质上是三个目标:
- 单一数据源:路由、菜单、权限规则尽量统一配置
- 类型安全:配置字段、权限字段、组件映射可检查
- 运行时可控:支持登录后动态生成、支持 403/404、支持懒加载
方案概览与取舍分析
在中后台项目里,常见有三种做法。
方案一:纯前端硬编码权限
把所有角色和权限规则写在前端配置里。
优点
- 上手快
- 本地开发简单
- 类型约束容易做
缺点
- 权限变更要发版
- 与后端规则容易不一致
- 多系统共享权限时维护成本高
方案二:后端返回完整菜单树,前端只渲染
登录后后端直接返回当前用户菜单和权限。
优点
- 权限中心化
- 前后端规则更统一
- 后端可按用户精确下发
缺点
- 前端路由组件映射复杂
- 若只返回菜单,不返回明确权限模型,按钮权限难统一
- 类型约束偏弱,尤其是组件路径映射
方案三:前端维护路由元数据,后端返回权限集
也就是本文推荐的折中方案:
- 前端维护全量路由配置
- 后端返回当前用户权限集/角色集
- 前端根据规则裁剪出可访问路由树和菜单树
优点
- 页面组件和路由强绑定,类型更稳
- 菜单和路由同源
- 支持后端动态权限,又不完全依赖后端下发菜单结构
缺点
- 前端要维护一份全量配置
- 路由裁剪逻辑要设计好
- 与后端要约定统一权限编码规范
如果你的项目是 React 中后台,页面数量中等到较多,我建议优先考虑方案三。它在可维护性和工程复杂度之间比较平衡。
核心原理
这套系统可以拆成四层:
-
权限模型层
定义角色、权限点、判断规则 -
路由配置层
在路由节点上声明meta信息,例如标题、图标、权限要求、是否显示在菜单中 -
裁剪与生成层
根据当前用户权限,从全量路由树中筛出可访问路由树和菜单树 -
运行时守卫层
在页面渲染时再次校验,防止用户手输 URL 进入无权限页面
架构图
flowchart TD
A[用户登录] --> B[后端返回角色集/权限集]
B --> C[前端加载全量路由配置]
C --> D[权限过滤器裁剪路由树]
D --> E[生成 React Router 路由]
D --> F[生成侧边菜单]
E --> G[访问页面时路由守卫二次校验]
G -->|通过| H[渲染业务页面]
G -->|拒绝| I[跳转 403]
classDiagram
class AppRoute {
+string path
+string key
+ReactNode element
+AppRoute[] children
+RouteMeta meta
}
class RouteMeta {
+string title
+boolean menu
+string icon
+string[] roles
+string[] permissions
+boolean hidden
}
class AuthContextValue {
+string[] roles
+string[] permissions
+boolean hasRole()
+boolean hasPermission()
+boolean canAccess()
}
AppRoute --> RouteMeta
权限模型设计
先不要急着写路由。第一步是把权限模型设计清楚。
1. 角色与权限点分离
角色适合做粗粒度控制,比如:
adminoperatorauditor
权限点适合做细粒度控制,比如:
dashboard.viewuser.readuser.editaudit.approve
一个比较稳妥的经验是:
- 页面级控制:角色 + 权限点都支持
- 按钮级控制:优先使用权限点
2. 路由元信息建议
每个路由节点除了 path 和 element,建议增加:
title: 菜单标题icon: 菜单图标标识menu: 是否参与菜单渲染hidden: 是否隐藏roles: 允许访问的角色permissions: 允许访问的权限点order: 菜单排序keepAlive/lazy:按项目需要扩展
3. 权限判断规则
最常见的规则有两类:
- 满足任一角色即可访问
- 满足任一权限点即可访问
当 roles 和 permissions 同时存在时,要提前定规则。本文采用:
只要满足
roles或permissions中任意一类即可访问;
如果两者都没配置,则认为公开可访问。
你也可以改成更严格的“同时满足”,但要统一,不然团队里每个人理解都不同。
实战代码(可运行)
下面给一套可落地的最小实现。为了聚焦核心逻辑,我使用 react-router-dom@6 风格。
目录结构示例
src/
app/
router.tsx
auth/
auth-context.tsx
access.ts
routes/
route-config.tsx
route-filter.ts
pages/
login.tsx
dashboard.tsx
user-list.tsx
audit.tsx
forbidden.tsx
not-found.tsx
menu/
side-menu.tsx
main.tsx
第一步:定义类型
// src/routes/route-config.tsx
import React from 'react';
export type Role = 'admin' | 'operator' | 'auditor';
export type Permission =
| 'dashboard.view'
| 'user.read'
| 'user.edit'
| 'audit.read'
| 'audit.approve';
export interface RouteMeta {
title: string;
menu?: boolean;
hidden?: boolean;
icon?: string;
roles?: Role[];
permissions?: Permission[];
order?: number;
}
export interface AppRoute {
key: string;
path: string;
element?: React.ReactNode;
children?: AppRoute[];
meta: RouteMeta;
}
这里我建议不要偷懒用 string[]。把权限点做成联合类型,IDE 自动提示会非常香,也能减少拼写错误。
第二步:定义页面
// src/pages/dashboard.tsx
export default function DashboardPage() {
return <div>仪表盘</div>;
}
// src/pages/user-list.tsx
export default function UserListPage() {
return <div>用户列表</div>;
}
// src/pages/audit.tsx
export default function AuditPage() {
return <div>审核中心</div>;
}
// src/pages/forbidden.tsx
export default function ForbiddenPage() {
return <div>403 Forbidden</div>;
}
// src/pages/not-found.tsx
export default function NotFoundPage() {
return <div>404 Not Found</div>;
}
// src/pages/login.tsx
export default function LoginPage() {
return <div>登录页</div>;
}
第三步:定义全量路由配置
// src/routes/route-config.tsx
import React from 'react';
import DashboardPage from '../pages/dashboard';
import UserListPage from '../pages/user-list';
import AuditPage from '../pages/audit';
export type Role = 'admin' | 'operator' | 'auditor';
export type Permission =
| 'dashboard.view'
| 'user.read'
| 'user.edit'
| 'audit.read'
| 'audit.approve';
export interface RouteMeta {
title: string;
menu?: boolean;
hidden?: boolean;
icon?: string;
roles?: Role[];
permissions?: Permission[];
order?: number;
}
export interface AppRoute {
key: string;
path: string;
element?: React.ReactNode;
children?: AppRoute[];
meta: RouteMeta;
}
export const appRoutes: AppRoute[] = [
{
key: 'dashboard',
path: '/dashboard',
element: <DashboardPage />,
meta: {
title: '仪表盘',
menu: true,
icon: 'dashboard',
permissions: ['dashboard.view'],
order: 1,
},
},
{
key: 'user',
path: '/users',
element: <UserListPage />,
meta: {
title: '用户管理',
menu: true,
icon: 'user',
roles: ['admin'],
permissions: ['user.read'],
order: 2,
},
},
{
key: 'audit',
path: '/audit',
element: <AuditPage />,
meta: {
title: '审核中心',
menu: true,
icon: 'audit',
roles: ['admin', 'auditor'],
permissions: ['audit.read'],
order: 3,
},
},
];
这里有一个关键点:菜单配置不要单独再写一份。
直接把菜单所需元数据挂在路由上,后面菜单树从路由树推导出来。
第四步:实现权限判断函数
// src/auth/access.ts
import type { AppRoute, Permission, Role } from '../routes/route-config';
export interface UserAuthInfo {
roles: Role[];
permissions: Permission[];
}
export function hasAnyRole(userRoles: Role[], routeRoles?: Role[]) {
if (!routeRoles || routeRoles.length === 0) return false;
return routeRoles.some((role) => userRoles.includes(role));
}
export function hasAnyPermission(
userPermissions: Permission[],
routePermissions?: Permission[]
) {
if (!routePermissions || routePermissions.length === 0) return false;
return routePermissions.some((permission) =>
userPermissions.includes(permission)
);
}
export function canAccessRoute(route: AppRoute, auth: UserAuthInfo) {
const { roles, permissions } = route.meta;
const noRoleLimit = !roles || roles.length === 0;
const noPermissionLimit = !permissions || permissions.length === 0;
if (noRoleLimit && noPermissionLimit) return true;
return (
hasAnyRole(auth.roles, roles) ||
hasAnyPermission(auth.permissions, permissions)
);
}
这部分逻辑很值得单独抽出来。因为它会被:
- 路由过滤
- 页面守卫
- 按钮权限
- 单元测试
重复使用。权限逻辑一旦散落,就很难改。
第五步:过滤可访问路由树
// src/routes/route-filter.ts
import type { AppRoute } from './route-config';
import type { UserAuthInfo } from '../auth/access';
import { canAccessRoute } from '../auth/access';
export function filterRoutes(routes: AppRoute[], auth: UserAuthInfo): AppRoute[] {
return routes
.filter((route) => canAccessRoute(route, auth))
.map((route) => {
const children = route.children
? filterRoutes(route.children, auth)
: undefined;
return {
...route,
children,
};
});
}
export function getMenuRoutes(routes: AppRoute[]): AppRoute[] {
return routes
.filter((route) => route.meta.menu && !route.meta.hidden)
.sort((a, b) => (a.meta.order ?? 0) - (b.meta.order ?? 0))
.map((route) => ({
...route,
children: route.children ? getMenuRoutes(route.children) : undefined,
}));
}
注意这里是先过滤权限,再生成菜单。
不要反过来,否则容易出现“菜单没问题,但路由还能访问”的分裂状态。
第六步:建立认证上下文
// src/auth/auth-context.tsx
import React, { createContext, useContext, useMemo } from 'react';
import type { Permission, Role } from '../routes/route-config';
import { hasAnyPermission, hasAnyRole } from './access';
interface AuthContextValue {
roles: Role[];
permissions: Permission[];
hasRole: (roles: Role[]) => boolean;
hasPermission: (permissions: Permission[]) => boolean;
}
const AuthContext = createContext<AuthContextValue | null>(null);
interface Props {
children: React.ReactNode;
roles: Role[];
permissions: Permission[];
}
export function AuthProvider({ children, roles, permissions }: Props) {
const value = useMemo<AuthContextValue>(
() => ({
roles,
permissions,
hasRole: (targetRoles) => hasAnyRole(roles, targetRoles),
hasPermission: (targetPermissions) =>
hasAnyPermission(permissions, targetPermissions),
}),
[roles, permissions]
);
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth 必须在 AuthProvider 内使用');
}
return context;
}
第七步:实现路由守卫
React Router v6 没有传统意义上的全局 beforeEach,所以通常做法是封装一个权限组件。
// src/app/router.tsx
import React from 'react';
import {
BrowserRouter,
Navigate,
Route,
Routes,
} from 'react-router-dom';
import { appRoutes, type AppRoute } from '../routes/route-config';
import { filterRoutes } from '../routes/route-filter';
import { useAuth } from '../auth/auth-context';
import { canAccessRoute } from '../auth/access';
import LoginPage from '../pages/login';
import ForbiddenPage from '../pages/forbidden';
import NotFoundPage from '../pages/not-found';
function GuardedRoute({ route }: { route: AppRoute }) {
const auth = useAuth();
if (!canAccessRoute(route, auth)) {
return <Navigate to="/403" replace />;
}
return <>{route.element}</>;
}
function renderRoutes(routes: AppRoute[]) {
return routes.map((route) => (
<Route
key={route.key}
path={route.path}
element={<GuardedRoute route={route} />}
/>
));
}
function AppRouterInner() {
const auth = useAuth();
const accessibleRoutes = filterRoutes(appRoutes, auth);
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
{renderRoutes(accessibleRoutes)}
<Route path="/403" element={<ForbiddenPage />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
);
}
export default function AppRouter() {
return (
<BrowserRouter>
<AppRouterInner />
</BrowserRouter>
);
}
严格来说,这里有一点“过滤 + 守卫”的重复判断。但这是值得的:
- 过滤:用于菜单和路由树生成
- 守卫:用于最终访问兜底
我通常把它理解为:一个做体验,一个做边界。
第八步:渲染侧边菜单
// src/menu/side-menu.tsx
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { appRoutes } from '../routes/route-config';
import { filterRoutes, getMenuRoutes } from '../routes/route-filter';
import { useAuth } from '../auth/auth-context';
export default function SideMenu() {
const auth = useAuth();
const location = useLocation();
const menuRoutes = getMenuRoutes(filterRoutes(appRoutes, auth));
return (
<aside>
<ul>
{menuRoutes.map((route) => (
<li key={route.key}>
<Link
to={route.path}
style={{
fontWeight: location.pathname === route.path ? 'bold' : 'normal',
}}
>
{route.meta.title}
</Link>
</li>
))}
</ul>
</aside>
);
}
第九步:入口整合
模拟一个当前登录用户。实际项目里,这部分通常来自登录接口或用户信息接口。
// src/main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import AppRouter from './app/router';
import { AuthProvider } from './auth/auth-context';
import type { Permission, Role } from './routes/route-config';
const roles: Role[] = ['auditor'];
const permissions: Permission[] = ['dashboard.view', 'audit.read'];
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<AuthProvider roles={roles} permissions={permissions}>
<AppRouter />
</AuthProvider>
</React.StrictMode>
);
这时候这个用户能访问:
/dashboard/audit
不能访问:
/users
权限与访问时序
sequenceDiagram
participant U as 用户
participant FE as 前端应用
participant BE as 后端接口
participant G as 权限守卫
U->>FE: 登录
FE->>BE: 请求用户信息/权限集
BE-->>FE: roles + permissions
FE->>FE: 过滤路由树与菜单树
U->>FE: 点击菜单或输入URL
FE->>G: 校验当前路由权限
G-->>FE: 允许/拒绝
FE-->>U: 页面内容或403
进阶:支持嵌套路由与布局
实际项目中,路由往往不是平铺的,而是:
- 顶层
Layout - 子路由挂在布局下
- 部分页面隐藏菜单但可访问详情页
一个简单设计是:
- 父节点负责布局与菜单分组
- 子节点负责页面级权限
- 详情页
hidden: true,但保留访问能力
示例:
// 片段示例
const nestedRoutes: AppRoute[] = [
{
key: 'system',
path: '/system',
element: <div>System Layout Outlet</div>,
meta: {
title: '系统管理',
menu: true,
roles: ['admin'],
},
children: [
{
key: 'system-users',
path: '/system/users',
element: <div>用户列表</div>,
meta: {
title: '用户列表',
menu: true,
permissions: ['user.read'],
},
},
{
key: 'system-user-detail',
path: '/system/users/:id',
element: <div>用户详情</div>,
meta: {
title: '用户详情',
hidden: true,
menu: false,
permissions: ['user.read'],
},
},
],
},
];
这里要注意一个架构决策:
父子权限是继承还是独立?
常见有两种:
-
父节点限制 + 子节点再限制
- 更安全
- 适合层级明确的管理系统
-
子节点完全独立判断
- 更灵活
- 容易出现父节点不可见但子节点可访问的情况
我的经验是:
菜单层看父节点,访问层看当前节点 + 可选叠加祖先权限。
如果系统偏严谨,建议做“祖先链权限合并校验”。
方案容量与复杂度估算
如果你的后台系统规模大概是:
- 路由 50~150 个
- 角色 5~20 个
- 权限点 100~500 个
那么本文这套方案在前端依然是可控的,复杂度主要在:
- 路由元数据维护
- 权限编码命名规范
- 页面/按钮粒度的一致性
什么时候该升级架构?
如果出现以下特征,可以考虑接入更完整的权限中心或后端动态菜单:
- 多个前端系统共用一套权限模型
- 权限变更非常频繁,不能依赖前端发版
- 需要租户隔离、数据域隔离、字段级权限
- 需要审计谁在何时拥有哪些权限
也就是说,本文方案很适合单系统或少量系统的中后台,但不是权限平台的终点形态。
常见坑与排查
这一部分我尽量写得接地气一点,因为真正让人头疼的,往往不是“不会写”,而是“看起来都对,就是不工作”。
1. 菜单显示正常,但手输 URL 还能访问
现象
- 侧边栏没有“用户管理”
- 但直接输入
/users能打开页面
原因
- 只做了菜单过滤,没有做页面守卫
- 或者守卫写在布局层,子页面漏掉了
排查方式
- 看是否存在
GuardedRoute - 看访问时是否再次调用
canAccessRoute
建议
- 菜单过滤和页面守卫一定都要做
- 把权限判断函数集中在一个模块里
2. 路由和菜单配置分离,后期一定打架
现象
- 新增页面时要改两处甚至三处
- 菜单标题、排序、图标和路由路径经常不一致
原因
- 菜单树单独维护
- 路由树单独维护
- 权限点还可能再维护一份
建议
- 使用“路由即菜单元数据源”
- 菜单只做投影,不做主配置
3. 父路由被过滤后,子路由也丢了
现象
- 明明子页面有权限,但因为父节点无权限,整个树都没了
原因
- 递归过滤逻辑按父节点先剪掉了整个分支
排查思路
- 看你的权限语义:父权限是否必须拥有?
- 看过滤逻辑是否允许“父不可见、子可见”
建议
- 先统一团队规则,再写代码
- 对后台管理系统,我更建议父节点有基础权限,子节点做细化限制
4. 权限点拼写错误,运行时才发现
现象
user.reda这种拼写,页面直接丢权限- 代码不报错
原因
- 权限字段用了裸
string
建议
- 权限编码尽量用 TypeScript 联合类型或常量枚举
- 至少在开发期加校验工具
例如:
export const PERMISSIONS = {
DASHBOARD_VIEW: 'dashboard.view',
USER_READ: 'user.read',
USER_EDIT: 'user.edit',
AUDIT_READ: 'audit.read',
AUDIT_APPROVE: 'audit.approve',
} as const;
export type Permission = (typeof PERMISSIONS)[keyof typeof PERMISSIONS];
5. 刷新页面后权限丢失
现象
- 登录后菜单正常
- 浏览器刷新后跳回 403 或空菜单
原因
- 权限数据只存在内存里
- 页面刷新时用户信息还没恢复完成
建议
- 使用 token + 用户信息恢复流程
- 路由渲染前增加初始化态,例如 loading skeleton
- 在权限数据未就绪前不要提前裁剪路由
安全最佳实践
这一段很重要,因为很多人会误以为“前端做了权限控制就安全了”。实际上并不是。
1. 前端权限控制本质上是体验层,不是最终安全边界
前端能做的是:
- 隐藏无权限菜单
- 阻止普通用户误操作
- 降低页面暴露度
但真正的安全边界必须在后端:
- 接口鉴权
- 数据范围校验
- 操作审计
结论很明确:页面看不见,不代表接口安全。
2. 页面权限和接口权限编码尽量统一
如果前端用的是:
user.readuser.edit
后端最好也按同样的权限编码返回和校验。这样有几个好处:
- 联调时认知一致
- 文档一致
- 排查权限问题更快
最怕的是前端写 user.read,后端是 GET_USER_LIST,中间再加一个映射层,维护成本会逐渐上升。
3. 对敏感页面使用懒加载 + 守卫组合
对一些管理页面、审核页面,可以使用懒加载减少首屏负担,同时在加载前就做权限判断。
import React, { lazy, Suspense } from 'react';
const AuditPage = lazy(() => import('../pages/audit'));
function AuditRoute() {
return (
<Suspense fallback={<div>加载中...</div>}>
<AuditPage />
</Suspense>
);
}
这样做的好处:
- 减少首次加载体积
- 非授权用户不会频繁加载无关页面资源
- 大型后台体验更稳
4. 不要把超复杂权限表达式硬塞进路由层
比如:
- A 角色在工作日可访问
- B 角色只能看自己部门数据
- C 角色审批额度不能超过某阈值
这些规则已经不是简单的页面路由权限,而是业务授权逻辑。
路由层只适合放“能否进入页面”的粗粒度判断,复杂规则应放到:
- 页面内部
- 接口返回
- 后端授权系统
性能最佳实践
权限系统看起来只是几次数组判断,但项目一大,性能问题就会慢慢冒出来。
1. 路由过滤结果做缓存或记忆化
如果用户权限不变,就没必要每次渲染都重新过滤整棵路由树。
const accessibleRoutes = React.useMemo(() => {
return filterRoutes(appRoutes, auth);
}, [auth.roles, auth.permissions]);
2. 权限集合优先用 Set
当权限点很多时,用 includes 在数组里查找会有额外开销。可以在运行时转成 Set。
export function hasAnyPermissionWithSet(
userPermissionSet: Set<string>,
routePermissions?: string[]
) {
if (!routePermissions || routePermissions.length === 0) return false;
return routePermissions.some((p) => userPermissionSet.has(p));
}
对中等规模项目,这不是必须;但如果按钮权限很多、页面里频繁判断,会更稳一些。
3. 按钮级权限判断别重复创建函数
例如在一个大表格里每行都渲染“编辑/删除/审核”按钮,如果每次 render 都临时创建判断逻辑,可能会引起额外渲染。
建议:
- 统一封装
PermissionButton - 配合
memo - 权限数据稳定化
例如:
// src/components/permission-button.tsx
import React from 'react';
import { useAuth } from '../auth/auth-context';
import type { Permission } from '../routes/route-config';
interface Props {
need: Permission[];
children: React.ReactNode;
}
export function PermissionButton({ need, children }: Props) {
const auth = useAuth();
const allowed = need.some((p) => auth.permissions.includes(p));
if (!allowed) return null;
return <>{children}</>;
}
边界条件与可执行建议
如果你准备在现有项目里重构权限路由,我建议按下面顺序做,不要一步到位全改:
第一阶段:先统一路由元数据
目标:
- 菜单和路由同源
- 页面访问守卫补齐
第二阶段:抽离权限判断中心
目标:
- 所有
role/permission判断都走统一函数 - 减少散落在组件内的硬编码
第三阶段:接入按钮级权限
目标:
- 封装
PermissionButton或usePermission - 页面内细粒度控制统一化
第四阶段:与后端权限编码对齐
目标:
- 权限点命名统一
- 联调与审计更容易
如果你的系统还很小,只有 5~10 个页面,不必把架构搞得过重;
但只要你已经出现“多个角色、多个菜单分组、详情页隐藏、按钮权限”这些特征,就值得尽早做这层抽象。
总结
可维护的权限路由与菜单系统,关键不在于“写几个判断”,而在于建立一套稳定的工程约束:
- 全量路由统一配置
- 菜单从路由元数据派生
- 权限判断集中封装
- 页面守卫和菜单过滤同时存在
- 前端做体验控制,后端做最终安全校验
- TypeScript 提供权限字段的类型约束
如果只记住一句话,我建议是:
不要把权限逻辑散落在页面里,要把它收敛成“配置 + 过滤 + 守卫”的统一体系。
这样做的好处不是今天少写几行代码,而是三个月后你再回头改角色、加菜单、接按钮权限时,系统不会轻易失控。
如果你正在做的是一个 React 中后台,这套方案基本足够覆盖中级项目的大部分场景;再往上走,才是多系统共享权限中心、租户隔离和更复杂授权模型的话题。