React 管理台的路由、权限与统一请求边界
本文目录
管理后台不是把页面放进侧边栏就结束了。登录入口、深层路由刷新、角色导航、接口认证、主题恢复和移动端菜单都要共享一致的边界。本文基于个人项目的 React 管理台源码静态审阅,整理这类结构的风险与改进方式;示例为独立伪代码,不包含原项目路径、品牌、业务字段或接口地址。静态阅读无法证明服务端权限配置、部署 rewrite 或浏览器运行结果,文中把这些列为验收项。
让 URL、路由与部署前缀一致
如果管理台部署在 /admin,应该把部署前缀当成应用路由的一部分统一管理:
<BrowserRouter basename="/admin">
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<RequireSession><AdminLayout /></RequireSession>}>
<Route path="/dashboard" element={<RequireRole role="admin"><Dashboard /></RequireRole>} />
<Route path="/browse" element={<BrowsePage />} />
</Route>
</Routes>
</BrowserRouter>
当 URL 前缀不一致时,路由可能空白、被 fallback 重定向,或在硬刷新时得到服务器 404。React Router 内部导航、页面硬跳转、登录成功后的 returnTo 和 Web 服务器的 SPA fallback 应使用同一个部署前缀。不要把未经校验的 returnTo 直接当作任意外站跳转地址;只接受应用内路径,并拒绝协议、主机名和反斜线等歧义输入。
发布验收至少直接打开并刷新 /admin/login、/admin/dashboard 和一个嵌套详情 URL;同时检查静态服务器把应用路径回退到正确的 HTML 文件,而 /api 等后端路径仍交由 API 服务处理。
前端路由门禁不是授权
路由门禁能改善导航体验,却不能保护数据。用户可以绕过前端组件直接请求接口,因此每个受保护的后端 handler 都必须验证令牌、账号状态、资源归属和角色权限。菜单隐藏、PrivateRoute 和客户端 user.role 都只能当界面状态,不能成为安全判据。
可以将“有会话”和“有角色权限”拆成两种明确状态:
function RequireSession({ children }: React.PropsWithChildren) {
const session = useSession()
if (session.status === 'loading') return <RouteLoading />
if (session.status !== 'authenticated') return <Navigate to="/login" replace />
return children
}
function RequireRole({ role, children }: React.PropsWithChildren<{ role: string }>) {
const session = useSession()
if (!session.roles.includes(role)) return <Navigate to="/forbidden" replace />
return children
}
真实实现还应覆盖加载状态,避免应用启动时把尚未恢复的会话误判为匿名;权限不足应有明确 403 页面,不能静默跳到一个看似成功的普通页面。后端仍须独立拒绝未授权请求。
一次源码复核发现了一个具体的 fail-open 写法:当 user 为空时,原角色判断会跳过拒绝分支并继续渲染受限页面。它说明“有登录标志”与“有可校验的角色”不能混成一个判断。客户端至少应这样失败关闭:
if (roles && (!user || !roles.includes(user.role))) {
return <Navigate to="/forbidden" replace />
}
这只修正界面门禁的默认行为,不构成服务端授权证据,也不能替代 API 对角色、账号状态和资源归属的验证。
统一请求与会话失效
集中创建 Axios client,适合统一 base URL、timeout、Bearer token、响应 envelope 和错误提示。需要审查的不只是“有没有拦截器”,还包括:
- API base URL 是否按构建环境配置;正式产物不能意外使用本地地址。
- 登录、刷新令牌、公开接口和普通受保护请求是否采用不同认证策略。
- 只对明确的未认证响应执行登出;权限不足、业务错误和网络离线不能被误判为过期。
- 同时清理 token、Zustand 内存态、持久化的 user/role 和账户缓存。
- 多个并发请求同时收到 401 时只触发一次会话失效流程,并取消或隔离旧会话中的请求。
- 记录错误时过滤 Authorization、Cookie 和个人资料;提示面向用户,诊断细节留给受控日志。
尤其要小心部署前缀:如果应用在 /admin 下,而响应拦截器硬编码跳到站点根部 /login,浏览器会离开管理台的路由空间。可让路由层处理过期状态;若确实需要整页跳转,则从经过校验的运行时配置构造地址,并为带前缀部署写浏览器测试。
同一份源码还出现了与此前缀问题相连的状态清理缺口:401 拦截器删除了单独保存的 token,却没有同步清除持久化的用户和 isAuthenticated。重新进入管理台时,界面可能恢复旧身份并继续显示受限页面,直到后续 API 再次拒绝请求。应让拦截器调用统一的会话失效动作,由它原子清理 token、用户与角色状态,并通过路由层在 /admin 前缀内回到登录页。这里确认的是客户端源码行为;没有据此推断服务器存在越权。
持久化只保留必要状态
Zustand persist 适合保存用户偏好,例如主题模式和有限枚举的主题色。认证信息则要格外审慎:把 isAuthenticated: true 长期存进 localStorage,不等于服务端会话仍然有效。应用启动后应以服务端会话检查结果为准,持久化状态只能作为加载提示;退出、过期、换账号和权限变更都要原子地清理相关状态。
主题恢复也有首屏边界。若颜色映射由应用内固定 token 提供,应将存储值限制为合法索引或名称;无效值回退默认。暗色 class 要在水合时正确恢复,防止亮暗主题闪烁。改变 document.documentElement 的代码只在浏览器执行,SSR 或测试环境中需隔离宿主 API。
桌面侧栏与移动导航
后台桌面侧栏和移动底部导航可以共享同一份路由元数据,但不能简单地把所有项目压进一条可横向滚动的细导航。移动端应优先暴露高频主入口,其余操作放进“更多”或分组抽屉;当前路由要有文字/图标之外的可访问状态,点击目标考虑安全区与键盘焦点。桌面折叠状态要避免内容区宽度与侧栏不同步,长菜单项需有 tooltip 或可换行规则。
对着实际小屏检查横向溢出、底部安全区、滚动容器和固定栏遮挡。仅看响应式 CSS 或桌面缩窄窗口,不能代表真实触屏体验。
管理台审查矩阵
| 范围 | 回归场景 |
|---|---|
| 路由前缀 | 直接打开、刷新、登录返回、未知路径和 /admin 子路径 |
| 会话恢复 | 冷启动、过期 token、多标签登出、切换账号和并发 401 |
| 角色权限 | 匿名、普通账户、管理员分别尝试 UI 导航与直接 API 请求 |
| 请求层 | base URL、超时、取消、错误 envelope、重试与敏感字段过滤 |
| 持久化 | 损坏/旧版本缓存、非法主题值、存储不可用和退出清理 |
| 响应式 | 320–390px 触屏宽度、横屏、底部安全区、长标签及键盘导航 |
建议把路由映射、角色策略和主题 token 都建成可测试的静态配置;把“前端隐藏了按钮”与“后端拒绝了请求”作为两条独立验收证据。没有后端代码、部署配置和真实会话时,结论必须明确标注为前端源码观察,而不是已经证明的线上漏洞。
参考资料
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。