基于 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 加密前缀保留(已加密凭证不受影响)。
237 lines
11 KiB
Markdown
237 lines
11 KiB
Markdown
# 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) —— 布局是业务的舞台,不是业务本身。
|