Files
LokiStack/docs/web-ui-guide/04-路由与权限控制.md
logan f9c6c1d826 feat: opsx-loki 全面升级 — 前端重构/用户与LDAP/部署向导/日志查询
基于 WEB-UI-Development-Guide 标准的整体升级,并更名 LokiDeploy → opsx-loki。

前端(Gin+React+shadcn/ui):
- 按 02/07 章重构:页面域化拆分(无超 400 行文件)、PageContainer/ListQuery、
  反馈分级(成功静默/失败 toast/危险确认)、401 单飞刷新+队列重放
- 暗色模式(next-themes)+ 语义 Token(--primary 品牌色/success/warning/info)
- 部署向导(状态推导四步:环境→存储→Loki→采集,内嵌表单闭环)
- 日志查询页:LogQL 标签构建器/拖拽直方图/日志上下文/Live Tail/查询历史
- 按用户+标签策略的查询授权(服务端强制合并策略)

后端(Go+Gin+JSON DB):
- 简单用户管理(仅 admin,无 RBAC)+ TokenVersion 会话吊销 + 登录 IP 限流
- LDAP 接入:配置(AES 加密存储/hasPassword 脱敏)/测试连接/批量导入/登录
- 存储配置测试连接(SigV4 零依赖)+ 桶不存在自动创建
- Loki 三模式部署修复:SSH 密钥认证、3.x 配置模板、ruler 对象存储嵌套块、
  凭证解密、孤儿 deploying 状态恢复、Grafana 部署(可选默认关)+面板导入
- DaemonSet 采集纯 YAML(去 helm 依赖)+ DNS 自愈回退 ClusterIP + 集群名标签
- 日志查询代理:cluster-svc 经 kube-apiserver proxy/url 直连 + cursor 翻页
- 统一 log 命名空间;部署就绪探活;Grafana 访问地址自动记录

修复:run.sh 增量编译检测、Sanitized 脱敏补全、LokiDeployment Update 白名单、
健康检查 CreatedAt 零值等 10+ 存量 bug。

数据兼容:DB 沿用旧 lokideploy.json(新名优先);localStorage 一次性迁移;
HKDF 加密前缀保留(已加密凭证不受影响)。
2026-10-24 13:20:12 +08:00

11 KiB
Raw Permalink Blame History

04 — 路由与权限控制

参考:vue-vben-admin(RouteMeta 30+ 字段、三种 access 模式)、soybean-admin(文件路由 + static/dynamic 一键切换)、ant-design-pro(access 纯函数)、vue-pure-admin(两级按钮权限);补充:gin-vue-admin(动态路由链路)


一、路由组织:静态、动态与混合

1.1 三种模式

frontend 模式:前端静态全量路由 + 按角色过滤
  优点 简单、无接口依赖      缺点 菜单改动要发版
backend 模式:后端下发菜单树 + import.meta.glob 组件映射(vben / pure / gva)
  优点 运营可配菜单          缺点 链路长、需处理守卫竞态
mixed 模式:本地静态路由 + 按 name 合并后端菜单(vben mergeRoutesByName)
  优点 开发体验与可配置性兼得 缺点 命名纪律要求高

soybean 的成熟做法:用 .env 的 VITE_AUTH_ROUTE_MODE=static|dynamic 一键切换两种模式(静态按 meta.roles 过滤本地路由 / 动态接收后端下发 ElegantConstRoute),两态殊途同归后统一装配菜单——模式切换是配置而非代码,底座工程的成熟标志。

推荐(多数中后台):「路由静态、菜单动态」的轻混合——路由决定 chunk 划分与守卫行为,宜稳;菜单决定导航展示,宜活。需要运营完全可配时再上 backend 模式。

1.2 backend 模式的实现链路(补充:gva 全链路;vben generateAccessible 同类实现更工程化)

POST /menu/getMenu(菜单树,component 为字符串路径 'view/layout/index.vue')
  → import.meta.glob('../view/**/*.vue') 按路径字符串匹配懒加载组件(pure 同款)
  → 守卫 addRoute(asyncRouterFlag 防重复注册)
  → 多级菜单递归扁平化为「layout + 二级路由」(pure:keep-alive 只支持二级)
  → routeMap(name→route)支撑菜单/keep-alive/参数查询

三个易踩坑,标杆都给了答案:

  1. 守卫竞态:动态 addRoute 后当前导航拿不到新路由 → 注册后需 return to.fullPath 重新 resolve 一次(vben:generateAccess 后 re-resolve)
  2. 防重复注册:asyncRouterFlag / isAccessChecked 标志位(pure:刷新时 wholeMenus.length === 0 才重建)
  3. 对象冻结:to.meta.matched = [...to.matched] 浅拷贝进 meta 供面包屑使用(直接引用会被冻结)

soybean 的差异化:文件路由自动化(@elegant-router)——src/views 目录即路由,生成物含 RouteKey/RouteMap 字面量联合类型,routerPush({ name: 'user-center' }) 全程类型安全。开发者不写路由文件,新增页面自动获得路由与菜单。


二、RouteMeta 规范:meta 驱动一切 UI 行为

2.1 vben 的启示

vben 给 RouteMeta 定义了 30+ 强类型字段(全 JSDoc 注释),菜单/标签页/面包屑/缓存/权限行为全部由 meta 声明,布局组件只消费 meta 不写死逻辑:

authority?: string[]        // 允许的角色
ignoreAccess?: boolean      // 免鉴权(如关于页)
menuVisibleWithForbidden?: boolean  // 菜单可见但访问 403(三态权限关键)
hideInMenu / hideInTab / hideInBreadcrumb / hideChildrenInMenu
keepAlive?: boolean         // 组件缓存
order?: number              // 菜单排序
affixTab?: boolean          // 标签页固定
iframeSrc / link / activePath / noBasicLayout

soybean 同款(17 字段强类型扩展 declare module 'vue-router'),pure 用 CustomizeRouteMeta + 逐字段中文注释。

2.2 标准 meta 集(必须的最小集)

// 每条路由必须声明的能力面
{
  path: '/users',
  component: () => import('@/views/user/Users.vue'),
  meta: {
    title: '用户管理',          // document.title 与面包屑用
    layer: 'system',           // shell | system | examples(可裁剪分层归属,底座工程必须)
    icon: 'user',              // 菜单图标(图标名是数据)
    permissions: ['user.view'],// 命中任一即放行;'*'/admin/超管角色 短路
    roles: ['admin'],          // 可选,角色级收紧
    keepAlive: false,          // 需要缓存的页面必须同时写 defineOptions name
    hideInMenu: false,
  }
}

纪律:meta 是路由的唯一能力声明处,且必须有类型契约(router.d.ts 模块扩充)。禁止「路由里一个 permission、菜单表里又一个 permission」双源人工同步。


三、守卫链设计

3.1 标准守卫流水线

① 白名单直通(login/mfa/403/500 等 shell 路由)
② token 检查(无 token → login?redirect=encodeURIComponent(fullPath))
③ 已登录访问 login → 重定向首页
④ 权限检查(meta.permissions 任一命中 / meta.roles)
⑤ NProgress + document.title + 主区滚动复位
⑥ onError:chunk 加载失败 → reload 一次(防发版后旧 chunk 404)

第 ⑥ 条与 adp 的 ErrorBoundary 识别 ChunkLoadError → retryCount 作 key 强制重挂载 lazy 异曲同工(一个在路由层救,一个在组件层救)——发版后旧 hash chunk 404 是必现事故,必须有恢复机制。

3.2 防开放重定向(安全底线,必须)

// adp: src/pages/user/login —— redirect 只允许同源相对路径
function getSafeRedirectUrl(redirect?: string) {
  if (!redirect || /^https?:\/\//i.test(redirect)) return '/'
  return redirect.startsWith('/') ? redirect : `/${redirect}`
}

redirect 参数编码传输 + 回跳侧协议黑名单校验(禁止 //evil.com、https://evil.com)。

3.3 React 端对照

  • RequireAuth(未登录跳转 + 403 渲染)结构正确即可
  • 路由渲染必须声明式(配置数组 + <Route>),权限字段语义与 Vue 端 meta 同名对齐

四、权限三粒度

4.1 粒度模型

路由级(页面能否访问)
  meta.permissions / meta.roles —— 守卫拦截,403 兜底页
菜单级(导航显不显示)
  服务端菜单 + 前端模块开关兜底过滤
按钮级(操作能不能点)
  指令 / 组件 / 函数 —— 至少支持一种,语义全站统一

4.2 权限三态(vben 的关键设计)

态 行为 meta
不可见 菜单过滤 + 路由拒绝 默认(authority 不命中)
可见但禁止 菜单显示,点进去 403 menuVisibleWithForbidden: true
放行 全通过 authority 命中 / ignoreAccess

无权限但想让用户知道功能存在的运营场景,用态 2(前端只需 403 页 + 菜单过滤豁免,成本低)。

4.3 按钮级实现对照

实现 出处 语义 失败行为
指令 v-auth gva directive/auth.js 角色串比对,.not 修饰符 el.remove()(DOM 摘除)
指令 v-access:code vben 权限码/角色双模式 el.remove()
函数 hasAuth() soybean 权限码,一个 hook 覆盖 90% 场景(不造指令/组件) v-if 不渲染(纯函数可单测)
双轨 v-auth/v-perms pure 路由 meta.auths(功能级)与登录 permissions(接口级)两级,*:*:* 超管通配 el.remove()
组件 <AccessControl> vben 包裹式 插槽不渲染(可配 fallback)

规范:指令用于「无权就不存在」;需要「无权置灰 + tooltip 解释」时用组件包裹。禁止用 v-if + 手写权限字符串比较 绕开统一入口(权限字符串散落是审计灾难)。soybean 的取向值得参考:权限判断优先做成可组合函数(逻辑可单测、不依赖模板编译),指令只是函数的语法糖。

4.4 权限计算要纯函数化(adp)

// src/access.ts —— 权限是 initialState 的纯函数,可单测穷举
export default function access(initialState) {
  const { currentUser } = initialState ?? {}
  return {
    canAdmin: currentUser && currentUser.access === 'admin',
    canUser: (code: string) => currentUser?.permissions?.includes(code),
  }
}

禁止在前端硬编码「角色→权限」映射表——后端 RBAC 是唯一源真相,前端硬编码映射必然脱节,属死代码。


五、菜单生成

5.1 两种数据流

A. 路由驱动(adp):config/routes.ts 集中声明 → ProLayout 自动生成菜单
   name 字段即 i18n key(menu.xxx),菜单名与路由单源
B. 接口驱动(vben/pure/gva):GET /auth/menu → 前端自建树 → 递归渲染
   后端可配(hidden/icon/order/外链),前端兜底过滤

5.2 菜单组件规范(必须)

  • 前端按分组规则(如 workspace→business→system 三段)建树,SYSTEM_ORDER 类常量定序
  • 模块开关兜底过滤:菜单接口没过滤掉的,前端再筛一次(后端是源真相,前端是最后防线)
  • 加载失败显示重试按钮,不留空白侧栏(菜单是导航生命线)
  • 高亮:外链/iframe 路由用 meta.activePath 指回正确菜单项
  • 菜单数据不持久化(重登重拉,防旧菜单闪现)

六、最佳实践清单

✅ 要做

  1. 新路由必须写全 meta 最低三件:title / layer(底座工程)/ permissions
  2. keepAlive 路由的页面组件必须 defineOptions({ name })(keep-alive include 按组件名匹配;pure/gva 以 vite 插件扫描校验)
  3. redirect 参数编码传输 + 回跳侧同源校验
  4. 权限判断统一走 permission 工具层,语义双端同名
  5. 路由 meta 有类型契约(router.d.ts 模块扩充)

❌ 不要

  1. 不要在前端硬编码角色→权限映射(后端 RBAC 是唯一源真相)
  2. 不要用 v-if 手写权限字符串比较
  3. 不要在守卫里做异步重活(拉菜单/用户信息放登录编排或首屏初始化,守卫只做检查)
  4. 不要让示例层路由 import 系统层组件(破坏可裁剪性)
  5. 不要权限配置文件双份并存(漂移源头)

七、AI 检查清单(对比目标工程逐项核对)

P0

  1. 路由 meta 是否有类型契约?是否存在「路由权限 + 菜单权限」双源人工同步?
  2. redirect 回跳是否有同源校验(防开放重定向)?
  3. 是否存在前端硬编码「角色→权限」映射(应删)?
  4. chunk 加载失败是否有恢复机制(onError reload / ErrorBoundary)?

P1

  1. keepAlive 链路是否完整(meta.keepAlive + defineOptions name + include 匹配)?缺 name 的缓存页面列出
  2. 按钮权限是否统一入口(指令/组件/函数三选一,禁止散落 v-if 比较字符串)?
  3. 菜单失败是否有重试 UI?菜单是否被错误地持久化?
  4. 路由渲染(React 端)是否声明式?

P2

  1. 是否值得引入文件路由自动化(soybean @elegant-router / pure router/modules/ glob 收集)?
  2. 是否需要权限三态中的「可见但 403」(运营诉求)?

下一篇:05-布局系统与导航设计 —— 布局是业务的舞台,不是业务本身。