Files
LokiStack/docs/web-ui-guide/04-路由与权限控制.md
T
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

237 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 不写死逻辑:
```ts
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 集(必须的最小集)
```js
// 每条路由必须声明的能力面
{
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 防开放重定向(安全底线,必须)
```ts
// 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)
```ts
// 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**
5. keepAlive 链路是否完整(meta.keepAlive + defineOptions name + include 匹配)?缺 name 的缓存页面列出
6. 按钮权限是否统一入口(指令/组件/函数三选一,禁止散落 v-if 比较字符串)?
7. 菜单失败是否有重试 UI?菜单是否被错误地持久化?
8. 路由渲染(React 端)是否声明式?
**P2**
9. 是否值得引入文件路由自动化(soybean @elegant-router / pure `router/modules/` glob 收集)?
10. 是否需要权限三态中的「可见但 403」(运营诉求)?
---
> 下一篇:[05-布局系统与导航设计](05-布局系统与导航设计.md) —— 布局是业务的舞台,不是业务本身。