基于 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 加密前缀保留(已加密凭证不受影响)。
11 KiB
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/参数查询
三个易踩坑,标杆都给了答案:
- 守卫竞态:动态 addRoute 后当前导航拿不到新路由 → 注册后需
return to.fullPath重新 resolve 一次(vben:generateAccess后 re-resolve) - 防重复注册:
asyncRouterFlag/isAccessChecked标志位(pure:刷新时wholeMenus.length === 0才重建) - 对象冻结:
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指回正确菜单项 - 菜单数据不持久化(重登重拉,防旧菜单闪现)
六、最佳实践清单
✅ 要做
- 新路由必须写全 meta 最低三件:title / layer(底座工程)/ permissions
- keepAlive 路由的页面组件必须
defineOptions({ name })(keep-alive include 按组件名匹配;pure/gva 以 vite 插件扫描校验) - redirect 参数编码传输 + 回跳侧同源校验
- 权限判断统一走 permission 工具层,语义双端同名
- 路由 meta 有类型契约(
router.d.ts模块扩充)
❌ 不要
- 不要在前端硬编码角色→权限映射(后端 RBAC 是唯一源真相)
- 不要用 v-if 手写权限字符串比较
- 不要在守卫里做异步重活(拉菜单/用户信息放登录编排或首屏初始化,守卫只做检查)
- 不要让示例层路由 import 系统层组件(破坏可裁剪性)
- 不要权限配置文件双份并存(漂移源头)
七、AI 检查清单(对比目标工程逐项核对)
P0
- 路由 meta 是否有类型契约?是否存在「路由权限 + 菜单权限」双源人工同步?
- redirect 回跳是否有同源校验(防开放重定向)?
- 是否存在前端硬编码「角色→权限」映射(应删)?
- chunk 加载失败是否有恢复机制(onError reload / ErrorBoundary)?
P1
- keepAlive 链路是否完整(meta.keepAlive + defineOptions name + include 匹配)?缺 name 的缓存页面列出
- 按钮权限是否统一入口(指令/组件/函数三选一,禁止散落 v-if 比较字符串)?
- 菜单失败是否有重试 UI?菜单是否被错误地持久化?
- 路由渲染(React 端)是否声明式?
P2
- 是否值得引入文件路由自动化(soybean @elegant-router / pure
router/modules/glob 收集)? - 是否需要权限三态中的「可见但 403」(运营诉求)?
下一篇:05-布局系统与导航设计 —— 布局是业务的舞台,不是业务本身。