2215 字
约 7 分钟
14
S14-A23 · 路由守卫:为什么"权限对了,体验还是差"

一个后台系统的前端用路由守卫做权限:无权限就 redirect 到 403 页。上线后用户体验很差——页面先渲染一半、再跳走,而"菜单里仍然显示无权限的入口",于是"点了才知道不行"。

路由守卫解决的是"能不能进",而它不解决"该不该显示入口"与"进入时数据是否就绪"。这个"混"的代价是"结构错位":"能不能进"是路由层,"该不该显示"是布局层,"数据就绪"是页面层——把它们都塞进守卫,等于"用一层解决三层的问题"。
三者混在一起,就是"权限对了但体验差"。

1. 守卫的三种职责

职责 时机
认证(有没有登录) 进入前
授权(有没有权限) 进入前
数据就绪(能不能渲染) 进入前/进入后

第三项如果放在"进入后",就会出现"渲染一半再跳"——因为组件已经挂载。第三项的"表现"很具体:页面边框与标题已经渲染,主体区域空白,然后突然跳走——用户看到的是"闪了一下",而它比"直接 403"更让人不安。

2. "菜单可见性"与"路由守卫"的关系

做法 体验
只做守卫(菜单全显示) 点了才知道不行
菜单按权限过滤 一眼就知道能做什么
两者都做 最好(防"手输 URL")

第二项的"实现"是"权限过滤在"菜单数据"上做,而不是在"渲染时判断""——于是"看不到的入口"连"渲染成本"都不产生。
这个"友好"的量化是"点击后的失败率":菜单全显示时,无权用户点进去的失败率可能 30%——而"过滤后"接近 0。
第二项是"体验"的关键:"看不到"比"点了被拒"友好——而它不能替代守卫,因为"URL 可以手输"。

3. "守卫里做什么"的三条原则

原则 说明
只做"快速判断"(本地状态) 不要"等接口"
慢判断放到"数据加载"里 用 loading 状态
拒绝时"给出口" 403 页要有"返回/申请"

第一项是"体验"的核心:**守卫里等一个接口 800ms,用户看到的是"白屏 800ms"**第一项的"替代"是"乐观进入 + 页面内降级":先放行、在页面里用 skeleton 等"细粒度权限",权限不足时"局部提示"——于是"不白屏"。
——而它比"快速跳转 + 目标页 loading"更差。

// 好:守卫只做本地判断(token/权限已在内存)
router.beforeEach((to) => {
  if (to.meta.requiresAuth && !auth.hasToken()) return { name: "login" };
  if (to.meta.perm && !auth.can(to.meta.perm)) return { name: "forbidden" };
  return true;
});
// 慢判断(如"这个单据能不能看")放到目标页的 loader,用 skeleton 表达

"守卫里该不该等接口"可以用一个"三百毫秒原则"判:

// 三种判断的成本与做法
< 50ms(本地状态):守卫里直接判,同步返回
50–300ms(缓存命中):守卫里等,但要有"过渡态"(进度条/骨架)
> 300ms(要请求):移出守卫 → 目标页 loading
// 判据:用户能忍受的"白屏"上限约 300ms

4. "权限数据从哪来"的三种形态

形态 问题
登录时一次性返回 权限变了要重新登录
每次进入拉一次 多一次请求(守卫里等)
内存 + 定期刷新 折中(推荐)

第三项的"细节"是"刷新失败要保留旧权限":网络抖动时"清空权限"会让用户"突然什么都看不到"——而它看起来像"登录失效"。
第三项是"实践中最稳"的:登录后把权限放内存,另起一个"低频刷新"——于是"守卫不用等",而"权限变更"能在几分钟内生效。

"守卫的快慢"要能"看见",否则"白屏 800ms"只会被当成"网络慢":

// 三段计时(开发期打开,生产采样)
router.beforeEach((to) => {
  const t0 = performance.now();
  const r = decide(to);                       // 同步判断
  const dt = performance.now() - t0;
  if (dt > 50) console.warn(`guard slow: ${to.path} ${dt.toFixed(0)}ms`);
  return r;
});
// 判据:> 50ms 的守卫会被用户感知为"点了没反应"

5. "守卫的三种失败"

失败 表现
死循环重定向 A→B→A
刷新后丢失状态 内存权限没了
403 页没有出口 用户只能改 URL

第一项的成因很典型:**"未登录 → 跳 login",而 login 页也标了 requiresAuth**第一项的"防法"是"守卫第一行判断"目标是否在白名单"":白名单里的路由直接放行——于是"login/403/静态资源"不会被拦。
——于是无限跳。

"403 页要给出口"是体验底线,而它只需要三个按钮:

<!-- 403 的三个出口(缺一个用户就只能改 URL) -->
<a href="/">返回首页</a>
<a href="/apply">申请该权限</a>
<button onclick="history.back()">返回上一页</button>
<!-- 额外:显示"你缺少哪个权限",并对上"找谁申请" -->

6. 排错:五种"路由相关问题"

现象 成因 处理方向
渲染一半跳走 判断在组件内 移到守卫/loader
刷新后掉登录态 状态只在内存 持久化 token
菜单显示无权入口 未按权限过滤 过滤菜单
无限重定向 登录页也被拦 白名单
手输 URL 能进 只做前端过滤 后端也要校验

第五项是"安全"底线:前端守卫只是"体验",不是"安全"——真正的授权必须在后端。

"权限变了"要在"不重新登录"的情况下生效——而它靠"低频刷新 + 失败保留":

// 三种刷新策略
1 每次进入都拉 → 守卫里等(差)
2 定时拉(5 分钟)+ 内存 → 守卫同步(好,推荐)
3 只在登录时拉 → 权限变更要重登(差)
// 判据:权限变更能否"几分钟内生效且不打断用户"

7. 边界:什么情况不必复杂守卫

场景 为什么 做法
单角色(全员同权限) 无授权分支 只做登录判断
内部工具(网络隔离) 风险低 简化
纯静态页(公开) 无需守卫 不做
后端渲染(SSR 带权限) 服务端已拦 前端可省

判断口径一句话:"未授权的人能不能"绕过前端"直接请求接口"。能(后端没校验),前端守卫只是"体验层",必须先补后端;不能(后端已拦),前端守卫就只该做"体验"。


最后一个"安全"提醒,值得单独说一遍:前端守卫可以被绕过——用户可以"改 URL、改内存状态、直接调接口"。所以"前端守卫"的正确定位是"减少无效请求",而"授权"必须"在后端每个接口上做"。把这两件事混淆的团队,通常会在"某个接口忘了加权限注解"时被穿透——而前端看起来"拦得很严"。

还有一个"壳层"细节:权限过滤要作用在"菜单数据"上,而不是"点击时判断"。前者让"不可见的入口"连 DOM 都不生成;后者会"生成再隐藏"——看起来一样,但"可搜索性/可访问性"上并不一样(屏幕阅读器可能读到它)。

附一条"命名"建议:把守卫分成 authGuard 与 permGuard 两个,而不是一个"大守卫"——于是"登录态变化"与"权限变化"能分别测试,也更容易定位"是哪一层拦下的"。

这条命名建议还有个副作用:它让"新增一种拦截"变成"加一个 guard",而不是"在一个函数里加 if"。

8. 自测

  1. 守卫的三种职责分别是什么时机?为什么"数据就绪"放在进入后会导致"渲染一半再跳"?
  2. 为什么"菜单过滤"不能替代"路由守卫"?两者的分工是什么?
  3. 为什么"守卫里等接口"体验更差?
  4. "权限放内存 + 低频刷新"解决了什么矛盾?
  5. "无限重定向"的典型成因是什么?
  6. 判断"前端守卫够不够"的那个问题是什么?
S14-A23 · 路由守卫:为什么"权限对了,体验还是差"
https://www.clxhxhhr.top/posts/6636/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。