成为全栈·Vue 管理后台篇·Vue Router如何守住多角色后台边界

侧栏里看不到"用户管理",不代表用户不能手动输入 /users。后台路由需要在导航入口建立清晰的体验边界,同时把真正的授权留给后端。本文以会员、编辑和管理员三种角色拆解 Vue Router 的访问控制。

本文代码快照:Vue 后台提交 d3ee41a。主要参考 manage-frontend-vue/src/router/index.ts、src/config/roles.ts、src/lib/permission.ts、src/config/menu.ts 和路由测试。

前言:菜单、路由和后端是三道不同的门

管理后台有三种用户入口:用户看见哪些菜单、用户直接访问某个 URL 是否进入页面、请求到达后端是否有权读取或修改数据。它们彼此有关,但不能混为一谈。

如果只隐藏菜单,用户仍可手动输入路径;如果路由挡住了页面,攻击者仍可用 curl 直接调用 API;如果只依赖后端,普通用户在界面里反复撞到 403,体验也很差。因此前端要分别做好导航呈现与页面访问控制,后端则必须对每个 API 做最终鉴权。

项目的角色定义直接来自契约语义:member 是普通会员,可使用个人中心能力;editor 管理内容域;admin 还可以管理用户和站点设置。代码将这些规则集中在权限函数中,而不是让组件各自判断角色字符串。

路由表用 meta 表达访问意图

Vue Router 的路由通过 meta 声明通用访问要求:

ts 复制代码
{
  path: 'articles',
  component: () => import('@/pages/articles/ArticleListPage.vue'),
  meta: { title: '文章管理', consoleOnly: true, capability: 'articles' },
}

路由懒加载让页面代码按需进入 bundle;consoleOnly 表示会员不进入后台控制台,capability 则对应更细的能力检查。像个人资料、通知、点赞和收藏属于已登录用户功能,可以要求 requiresAuth,但不需要 consoleOnly。页面层级和访问属性由路由配置集中可见,方便审阅和测试。

项目通过类型扩展定义 RouteMeta 字段,避免整个应用随意拼写字符串。能力键是一个有限联合类型:articles、comments、categories、tags、users、site。守卫将这些 key 映射到纯权限函数:

ts 复制代码
const capabilityCheck = {
  articles: canManageArticles,
  comments: canModerateComments,
  categories: canManageCategories,
  tags: canManageTags,
  users: canManageUsers,
  site: canManageSiteSettings,
} as const

这让路由声明可以使用能力名,而角色到能力的映射仍然只有一个实现位置。

守卫的判断顺序

全局 beforeEach 先判断当前是否存在完整会话,再依次检查 guest-only、登录要求、后台角色和能力权限:

ts 复制代码
router.beforeEach((to) => {
  const auth = useAuthStore(pinia)
  const signedIn = Boolean(auth.accessToken && auth.user)

  if (to.meta.guestOnly && signedIn)
    return auth.user?.role === 'member' ? '/no-access' : '/dashboard'
  if (to.meta.requiresAuth && !signedIn)
    return { path: '/login', replace: true, state: { from: to.fullPath } }
  if (to.meta.consoleOnly && auth.user?.role === 'member') return '/no-access'
  if (to.meta.capability && !capabilityCheck[to.meta.capability](auth.user)) return '/403'
  return true
})

顺序有实际意义。未登录用户访问受保护页面,会去登录并保留原目标;已登录会员访问后台控制台,会看到"不可进入控制台"的提示;编辑尝试打开用户管理页,则是已登录但缺少特定能力,进入 403。区分身份入口错误与能力不足,可以给用户更准确的说明。

登录页读取导航 state,成功后按授权角色进行默认落地,或返回先前目标。回跳路径必须来自 Router 保存的内部导航状态,避免直接把任意外部 URL 当作跳转目标。若路径指向登录后仍不可访问的页面,守卫仍会再次执行权限检查。

角色等级和能力函数

roles.ts 定义角色等级与默认首页,permission.ts 将契约最低角色转换成领域能力:

ts 复制代码
export const canManageArticles = (actor: Actor) =>
  roleAtLeast(actor?.role, 'editor')

export const canManageUsers = (actor: Actor) =>
  roleAtLeast(actor?.role, 'admin')

这里把"是否可以管理文章"作为业务问题,而不是在每个模板里写 user.role === 'admin'。未来契约可能允许另一角色获得某能力,页面和菜单可以继续引用同一判定函数,只需更新映射与对应测试。

某些资源操作还涉及资源所有者,例如会员可以修改自己的投稿,编辑可以管理全站文章。纯权限模块可以接受 actor 与 owner id,决定按钮是否展示;后端仍需逐请求验证所有权。角色能力函数是用户体验层和前端逻辑的一致性工具,不是安全令牌。

菜单与路由共用权限函数

菜单配置为每项声明可选 can 函数,visibleMenuGroups(actor) 过滤无权菜单:

ts 复制代码
{
  path: '/users',
  label: '用户管理',
  icon: PeopleOutline,
  can: canManageUsers,
}

如此一来,用户列表入口的可见性由 canManageUsers 决定,路由访问也调用同一个函数。避免出现"菜单看不到,但直接 URL 可以进"的前端逻辑矛盾。注意这仍然只保证前端两个入口一致,不能保证后端就授权正确。

路由和菜单的声明方式不同:路由用 meta.capability 便于守卫通用处理;菜单用 can 便于配置过滤。两者通过同一权限映射连接,不需要复制角色等级逻辑。

403、No Access 与 404 各自表达什么?

  • 登录页:需要身份,但当前没有有效会话。
  • No Access:会员账号已登录,但不能进入管理控制台;个人中心仍可用。
  • 403:用户已登录并进入控制台范围,但当前角色缺少目标功能的能力,例如编辑访问用户管理。
  • 404:路由不存在或目标资源无法找到。资源是否存在可能需要后端决定,前端的路由 404 只说明当前 URL 没有匹配页面。

把它们全部导向登录页会让用户以为账号状态有问题;把所有限制都叫 403 又掩盖了会员账号本来就不属于后台人员这一产品设计。

用路由测试钉住语义

项目的 router/index.test.ts 验证几个重要边界:匿名访问 /articles 会被送往登录;member 可以进入个人资料但不能进入文章后台;editor 可以进入文章列表但打开 /users 会得到 403。测试围绕角色、路径与最终地址,而非只测试某个 helper 返回 true/false,因此能检查守卫集成后的行为。

新增一个管理页面时,至少检查:

  1. 路由声明是否需要登录、后台角色或特定能力?
  2. 侧栏入口是否复用对应能力规则?
  3. 页面操作是否进一步涉及资源归属或状态转移?
  4. 后端端点是否有对应权限校验?
  5. 路由测试覆盖匿名、会员、编辑和管理员的预期路径了吗?

权限用例不必把所有角色和所有路由排列组合成庞大矩阵,但每个权限边界都要有代表性证明,尤其是"角色有后台入口、却没有某一管理能力"的情况。

最重要的边界:路由守卫不是后端鉴权

浏览器端代码可以被修改、绕过或完全跳过。用户可以在开发者工具中改 store,也可以直接请求 API。因此后端必须验证访问令牌、角色、资源归属和状态迁移;前端菜单与守卫只负责更友好的导航和早期反馈。

前端权限判断仍然值得做,因为它减少不必要页面加载和必然失败的操作,让不同角色看到符合职责的导航。但要准确描述这份工作:它"防止正常 UI 路径误入",不是"保护接口免受攻击"。真正安全边界位于服务器。

小结:共享规则,保持边界独立

Vue Router 用 meta 描述登录、控制台和能力要求;纯权限模块把角色映射为业务能力;菜单复用同一规则;测试验证实际导航结果。No Access、403 和 404 表达不同问题。后端独立执行授权,浏览器守卫不取代它。

下一篇:M7-09会继续讨论 Pinia 和 React Zustand:认证会话、服务器数据和界面偏好分别该由谁保存,哪些偏好适合持久化,哪些凭证不应进入 localStorage。

延伸阅读


如果这篇文章对你有帮助,欢迎订阅我的 CSDN 专栏 「成为全栈」:

🔗 专栏地址:https://blog.csdn.net/fungleo/category_13204651.html

📦 本系列配套代码仓库:https://github.com/fengcms/become-a-full-stack-developer

相关推荐
FungLeo9 分钟前
成为全栈·Vue 管理后台篇·分页只显示第一页:从接口total 定位到前端修复
前端·vue.js·状态模式·分页·naive ui·服务端分页·前端问题排查
ZhengEnCi33 分钟前
苹果滑动为什么有「肉感」:橡皮筋越界阻尼与弹簧动画的物理原理
前端
Moment40 分钟前
不会后期的我,发现了一个开源胶片神器😍😍😍
前端·后端·开源
L1mbo43 分钟前
Lite模式 缓解微信开发者工具卡顿,附 uni-app 实践方案
前端·微信小程序
FungLeo1 小时前
成为全栈·Vue管理后台篇·仪表盘不是几张卡片:组合统计接口与角色视图
前端·javascript·vue.js·rbac·仪表盘·管理后台·tanstack query
程序员大阳1 小时前
HTML+CSS+JS对话式实战(7)-学生端活动报名功能开发
javascript·css·html·活动报名
ruanno_top1 小时前
可搜索的下拉框,键盘按不动:焦点进了下拉,事件还挂在触发器上
vue.js
SameX1 小时前
HarmonyOS 做桌面卡片 onAddForm 要同步返回但数据在数据库里的解法
前端
deli0071 小时前
手机夜景模式为什么要举 3 秒?8 帧 RAW 堆栈 SNR 实测
前端