generator-mico-cli 0.1.29 → 0.2.2-8.beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +199 -15
- package/bin/mico.js +232 -27
- package/generators/micro-react/index.js +200 -18
- package/generators/micro-react/meta.json +13 -0
- package/generators/micro-react/templates/.commitlintrc.js +1 -0
- package/generators/micro-react/templates/.cursor/rules/always-read-docs.mdc +14 -4
- package/generators/micro-react/templates/.cursor/rules/cicd-deploy.mdc +10 -8
- package/generators/micro-react/templates/.cursor/rules/coding-conventions.mdc +1 -1
- package/generators/micro-react/templates/.cursor/rules/development-guide.mdc +3 -4
- package/generators/micro-react/templates/.cursor/rules/layout-app.mdc +38 -31
- package/generators/micro-react/templates/.cursor/rules/project-overview.mdc +7 -4
- package/generators/micro-react/templates/.cursor/rules/theme-system.mdc +10 -12
- package/generators/micro-react/templates/.eslintrc.js +25 -1
- package/generators/micro-react/templates/AGENTS.md +5 -2
- package/generators/micro-react/templates/CICD/before_build.sh +76 -0
- package/generators/micro-react/templates/CICD/start_dev.sh +27 -3
- package/generators/micro-react/templates/CICD/start_prod.sh +26 -3
- package/generators/micro-react/templates/CICD/start_test.sh +28 -3
- package/generators/micro-react/templates/CICD/wangsu_fresh_dev.sh +4 -4
- package/generators/micro-react/templates/CICD/wangsu_fresh_prod.sh +4 -4
- package/generators/micro-react/templates/CICD/wangsu_fresh_test.sh +4 -4
- package/generators/micro-react/templates/CLAUDE.md +16 -9
- package/generators/micro-react/templates/README.md +42 -4
- package/generators/micro-react/templates/_gitignore +4 -0
- package/generators/micro-react/templates/_npmrc +4 -0
- package/generators/micro-react/templates/apps/layout/config/config.dev.ts +33 -17
- package/generators/micro-react/templates/apps/layout/config/config.prod.development.ts +24 -29
- package/generators/micro-react/templates/apps/layout/config/config.prod.testing.ts +25 -6
- package/generators/micro-react/templates/apps/layout/config/config.prod.ts +16 -7
- package/generators/micro-react/templates/apps/layout/config/config.ts +27 -4
- package/generators/micro-react/templates/apps/layout/config/routes.ts +10 -5
- package/generators/micro-react/templates/apps/layout/docs/arch-/346/227/245/345/277/227/344/270/216/345/270/270/351/207/217.md +2 -2
- package/generators/micro-react/templates/apps/layout/docs/arch-/350/257/267/346/261/202/346/250/241/345/235/227.md +1 -1
- package/generators/micro-react/templates/apps/layout/docs/common-intl.md +372 -0
- package/generators/micro-react/templates/apps/layout/docs/feat-/346/236/204/345/273/272define/344/270/216/345/205/215/350/256/244/350/257/201/345/210/235/345/247/213/346/200/201.md +44 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-404/351/241/265/351/235/242.md +103 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/344/270/273/351/242/230/350/211/262/345/210/207/346/215/242.md +22 -26
- package/generators/micro-react/templates/apps/layout/docs/feature-/345/276/256/345/211/215/347/253/257/346/250/241/345/274/217.md +185 -28
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/217/234/345/215/225/346/235/203/351/231/220/346/216/247/345/210/266.md +420 -0
- package/generators/micro-react/templates/apps/layout/docs/feature-/350/267/257/347/224/261/344/270/216/350/217/234/345/215/225/350/247/243/350/200/246.md +179 -0
- package/generators/micro-react/templates/apps/layout/docs/fix-SSO/346/227/240/351/231/220/351/207/215/345/256/232/345/220/221.md +88 -0
- package/generators/micro-react/templates/apps/layout/docs/utils-timezone.md +324 -0
- package/generators/micro-react/templates/apps/layout/mock/api.mock.ts +81 -61
- package/generators/micro-react/templates/apps/layout/mock/menus.ts +114 -4
- package/generators/micro-react/templates/apps/layout/mock/pages.ts +86 -0
- package/generators/micro-react/templates/apps/layout/package.json +7 -4
- package/generators/micro-react/templates/apps/layout/src/app.tsx +122 -83
- package/generators/micro-react/templates/apps/layout/src/common/auth/index.ts +3 -0
- package/generators/micro-react/templates/apps/layout/src/common/helpers.ts +177 -0
- package/generators/micro-react/templates/apps/layout/src/common/locale.ts +22 -17
- package/generators/micro-react/templates/apps/layout/src/common/menu/parser.ts +283 -28
- package/generators/micro-react/templates/apps/layout/src/common/menu/types.ts +69 -5
- package/generators/micro-react/templates/apps/layout/src/common/micro/index.ts +34 -0
- package/generators/micro-react/templates/apps/layout/src/common/micro-prefetch.ts +109 -0
- package/generators/micro-react/templates/apps/layout/src/common/portal-data.ts +45 -0
- package/generators/micro-react/templates/apps/layout/src/common/request/config.ts +72 -10
- package/generators/micro-react/templates/apps/layout/src/common/request/index.ts +2 -2
- package/generators/micro-react/templates/apps/layout/src/common/request/interceptors.ts +31 -3
- package/generators/micro-react/templates/apps/layout/src/common/request/sso.ts +29 -11
- package/generators/micro-react/templates/apps/layout/src/common/request/url-resolver.ts +23 -8
- package/generators/micro-react/templates/apps/layout/src/common/route-guard.ts +345 -0
- package/generators/micro-react/templates/apps/layout/src/common/theme.ts +2 -4
- package/generators/micro-react/templates/apps/layout/src/common/upload/oss.ts +3 -4
- package/generators/micro-react/templates/apps/layout/src/common/upload/types.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/common/uploadFiles.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.less +8 -3
- package/generators/micro-react/templates/apps/layout/src/components/AppTabs/index.tsx +25 -8
- package/generators/micro-react/templates/apps/layout/src/components/HeaderDropdown/index.tsx +20 -0
- package/generators/micro-react/templates/apps/layout/src/components/IconFont/index.tsx +5 -6
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.less +21 -6
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/index.tsx +83 -107
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/micro-app-manager.ts +569 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/AvatarDropdown.tsx +383 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/avatar-dropdown.less +35 -0
- package/generators/micro-react/templates/apps/layout/src/components/RightContent/index.ts +2 -0
- package/generators/micro-react/templates/apps/layout/src/constants/index.ts +170 -6
- package/generators/micro-react/templates/apps/layout/src/global.less +19 -6
- package/generators/micro-react/templates/apps/layout/src/hooks/useMenu.ts +3 -2
- package/generators/micro-react/templates/apps/layout/src/hooks/useRoutePermissionRefresh.ts +72 -0
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.less +3 -1
- package/generators/micro-react/templates/apps/layout/src/layouts/components/header/index.tsx +10 -55
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.less +34 -4
- package/generators/micro-react/templates/apps/layout/src/layouts/components/menu/index.tsx +33 -9
- package/generators/micro-react/templates/apps/layout/src/layouts/index.less +84 -13
- package/generators/micro-react/templates/apps/layout/src/layouts/index.tsx +178 -47
- package/generators/micro-react/templates/apps/layout/src/locales/en-US.ts +12 -0
- package/generators/micro-react/templates/apps/layout/src/locales/zh-CN.ts +12 -0
- package/generators/micro-react/templates/apps/layout/src/pages/403/index.tsx +34 -0
- package/generators/micro-react/templates/apps/layout/src/pages/404/index.tsx +78 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.less +3 -0
- package/generators/micro-react/templates/apps/layout/src/pages/Home/index.tsx +7 -1
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.less +1 -1
- package/generators/micro-react/templates/apps/layout/src/pages/User/Login/index.tsx +9 -5
- package/generators/micro-react/templates/apps/layout/src/requestErrorConfig.ts +1 -1
- package/generators/micro-react/templates/apps/layout/src/services/config/index.ts +63 -0
- package/generators/micro-react/templates/apps/layout/src/services/config/type.ts +30 -0
- package/generators/micro-react/templates/apps/layout/src/services/user.ts +29 -2
- package/generators/micro-react/templates/apps/layout/tailwind.config.js +3 -0
- package/generators/micro-react/templates/deployDesc.md +3 -3
- package/generators/micro-react/templates/dev.preset.json +14 -0
- package/generators/micro-react/templates/docs/dev-preset.md +130 -0
- package/generators/micro-react/templates/package.json +21 -6
- package/generators/micro-react/templates/packages/common-intl/README.md +427 -0
- package/generators/micro-react/templates/packages/common-intl/package.json +34 -0
- package/generators/micro-react/templates/packages/common-intl/src/index.ts +7 -0
- package/generators/micro-react/templates/packages/common-intl/src/indexedDBUtils.ts +51 -0
- package/generators/micro-react/templates/packages/common-intl/src/intl.ts +50 -0
- package/generators/micro-react/templates/packages/common-intl/src/utils.ts +482 -0
- package/generators/micro-react/templates/packages/common-intl/tsconfig.json +22 -0
- package/generators/micro-react/templates/packages/common-intl/vite.config.ts +25 -0
- package/generators/micro-react/templates/scripts/apply-sentry-plugin.ts +45 -0
- package/generators/micro-react/templates/scripts/collect-dist.js +10 -0
- package/generators/micro-react/templates/scripts/dev-preset.js +265 -0
- package/generators/micro-react/templates/scripts/dev-preset.schema.json +39 -0
- package/generators/micro-react/templates/turbo.json +4 -1
- package/generators/subapp-react/index.js +326 -40
- package/generators/subapp-react/meta.json +10 -0
- package/generators/subapp-react/templates/homepage/.env +2 -1
- package/generators/subapp-react/templates/homepage/README.md +3 -3
- package/generators/subapp-react/templates/homepage/config/config.dev.ts +14 -7
- package/generators/subapp-react/templates/homepage/config/config.prod.development.ts +16 -5
- package/generators/subapp-react/templates/homepage/config/config.prod.testing.ts +16 -5
- package/generators/subapp-react/templates/homepage/config/config.prod.ts +14 -5
- package/generators/subapp-react/templates/homepage/config/config.ts +27 -0
- package/generators/subapp-react/templates/homepage/config/routes.ts +2 -2
- package/generators/subapp-react/templates/homepage/mock/api.mock.ts +2 -2
- package/generators/subapp-react/templates/homepage/package.json +7 -4
- package/generators/subapp-react/templates/homepage/src/app.tsx +18 -27
- package/generators/subapp-react/templates/homepage/src/common/request.ts +29 -2
- package/generators/subapp-react/templates/homepage/src/global.less +6 -5
- package/generators/subapp-react/templates/homepage/src/pages/index.less +3 -3
- package/generators/subapp-react/templates/homepage/src/pages/index.tsx +99 -60
- package/generators/subapp-react/templates/homepage/src/styles/theme.less +1 -1
- package/generators/subapp-umd/ignore-list.json +5 -0
- package/generators/subapp-umd/index.js +309 -0
- package/generators/subapp-umd/meta.json +11 -0
- package/generators/subapp-umd/templates/README.md +94 -0
- package/generators/subapp-umd/templates/package.json +35 -0
- package/generators/subapp-umd/templates/public/index.html +34 -0
- package/generators/subapp-umd/templates/src/App.less +15 -0
- package/generators/subapp-umd/templates/src/App.tsx +13 -0
- package/generators/subapp-umd/templates/src/index.ts +2 -0
- package/generators/subapp-umd/templates/tsconfig.json +27 -0
- package/generators/subapp-umd/templates/webpack.config.js +70 -0
- package/lib/utils.js +332 -2
- package/package.json +15 -2
- package/generators/micro-react/templates/apps/layout/mock/menus.json +0 -100
- package/generators/micro-react/templates/apps/layout/src/common/constants.ts +0 -38
- package/generators/micro-react/templates/apps/layout/src/components/MicroAppLoader/container-manager.ts +0 -202
- package/generators/micro-react/templates/packages/shared-styles/README.md +0 -124
- package/generators/micro-react/templates/packages/shared-styles/arco-design-mobile-override.less +0 -91
- package/generators/micro-react/templates/packages/shared-styles/arco-override.less +0 -119
- package/generators/micro-react/templates/packages/shared-styles/index.d.ts +0 -44
- package/generators/micro-react/templates/packages/shared-styles/index.less +0 -13
- package/generators/micro-react/templates/packages/shared-styles/package.json +0 -30
- package/generators/micro-react/templates/packages/shared-styles/theme-inject.less +0 -10
- package/generators/micro-react/templates/packages/shared-styles/themes/dark/custom-var.less +0 -290
- package/generators/micro-react/templates/packages/shared-styles/themes/normal/custom-var.less +0 -269
- package/generators/micro-react/templates/packages/shared-styles/variables-only.less +0 -433
- package/generators/micro-react/templates/packages/shared-styles/variables.less +0 -452
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
# 菜单权限控制
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-01-24 更新时间:2026-02-08
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
基于用户信息中的 `side_menus` 字段实现菜单和路由的白名单权限控制。非超级用户只能看到和访问 `side_menus` 中配置的菜单项,访问无权限路由时显示 403 页面。
|
|
8
|
+
|
|
9
|
+
**v2 更新**:支持认证(Authentication)与授权(Authorization)分离配置,可独立控制"跳过 SSO 登录"和"跳过菜单权限校验"。
|
|
10
|
+
|
|
11
|
+
## 核心概念
|
|
12
|
+
|
|
13
|
+
### 认证 vs 授权
|
|
14
|
+
|
|
15
|
+
| 概念 | 英文 | 作用 | 配置项 |
|
|
16
|
+
| ---- | -------------- | ------------------------ | ----------------------- |
|
|
17
|
+
| 认证 | Authentication | 确认用户身份(是否登录) | `noAuthRouteList` |
|
|
18
|
+
| 授权 | Authorization | 确认用户权限(能否访问) | `noPermissionRouteList` |
|
|
19
|
+
|
|
20
|
+
### 四种路由场景
|
|
21
|
+
|
|
22
|
+
| 场景 | noAuthRouteList | noPermissionRouteList | accessControlEnabled | 示例 |
|
|
23
|
+
| --- | --- | --- | --- | --- |
|
|
24
|
+
| 公开页面 | ✅ | ✅ | `false` | 首页、活动页 |
|
|
25
|
+
| 登录后公共页面 | ❌ | ✅ | - | 个人设置、帮助中心 |
|
|
26
|
+
| 需要权限的页面 | ❌ | ❌ | `true` | 后台管理、业务功能 |
|
|
27
|
+
| 登录页等特殊页面 | ✅ | ✅ | `false` | /login、/403 |
|
|
28
|
+
|
|
29
|
+
## 技术方案
|
|
30
|
+
|
|
31
|
+
### 技术栈
|
|
32
|
+
|
|
33
|
+
- 框架:React 18 + @umijs/max
|
|
34
|
+
- UI 组件:@mico-platform/ui (Result 组件)
|
|
35
|
+
- 状态管理:Umi initialState
|
|
36
|
+
|
|
37
|
+
### 权限控制流程
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
用户访问路由
|
|
41
|
+
│
|
|
42
|
+
▼
|
|
43
|
+
┌──────────────────────────────────────────────┐
|
|
44
|
+
│ 1. SSO 认证检查 (app.tsx) │
|
|
45
|
+
│ isNoAuthRoute(pathname)? │
|
|
46
|
+
│ ├── 是 → 跳过 SSO │
|
|
47
|
+
│ page.accessControlEnabled === false? │
|
|
48
|
+
│ ├── 是 → 跳过 SSO(PAGES 数据驱动) │
|
|
49
|
+
│ └── 否 → 执行 ensureSsoSession() │
|
|
50
|
+
└──────────────────────────────────────────────┘
|
|
51
|
+
│
|
|
52
|
+
▼
|
|
53
|
+
┌──────────────────────────────────────────┐
|
|
54
|
+
│ 2. 权限校验 (layouts/index.tsx) │
|
|
55
|
+
│ isNoPermissionRoute(pathname)? │
|
|
56
|
+
│ ├── 是 → 跳过权限校验 │
|
|
57
|
+
│ └── 否 → 双层权限判断 │
|
|
58
|
+
│ Tier 1: 页面在菜单中? │
|
|
59
|
+
│ ├── 是 → 沿用 sideMenus │
|
|
60
|
+
│ │ 白名单逻辑 │
|
|
61
|
+
│ └── 否 → Tier 2: 页面级权限 │
|
|
62
|
+
│ adminOnly / routeKey│
|
|
63
|
+
│ 详见:路由与菜单解耦文档 │
|
|
64
|
+
└──────────────────────────────────────────┘
|
|
65
|
+
│
|
|
66
|
+
▼
|
|
67
|
+
┌──────────────────────────────────────────┐
|
|
68
|
+
│ 3. 菜单渲染 (menu/index.tsx) │
|
|
69
|
+
│ 按 side_menus 过滤 │
|
|
70
|
+
│ 每个菜单项独立检查 isNoPermissionRoute│
|
|
71
|
+
│ ├── 匹配 → 该项始终显示 │
|
|
72
|
+
│ └── 不匹配 → 按 sideMenus 白名单过滤 │
|
|
73
|
+
└──────────────────────────────────────────┘
|
|
74
|
+
│
|
|
75
|
+
▼
|
|
76
|
+
┌──────────────────────────────────────────────┐
|
|
77
|
+
│ 4. 子应用加载 (MicroAppLoader) │
|
|
78
|
+
│ page.accessControlEnabled === false? │
|
|
79
|
+
│ ├── 是 → 直接加载(PAGES 数据驱动) │
|
|
80
|
+
│ isNoAuthRoute(pathname)? │
|
|
81
|
+
│ ├── 是 → 直接加载(静态配置兜底) │
|
|
82
|
+
│ isNoPermissionRoute(pathname)? │
|
|
83
|
+
│ ├── 是 → 直接加载 │
|
|
84
|
+
│ └── 否 → 等待 currentUser │
|
|
85
|
+
└──────────────────────────────────────────────┘
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 权限判断逻辑(详细)
|
|
89
|
+
|
|
90
|
+
```
|
|
91
|
+
用户访问 /some-path
|
|
92
|
+
│
|
|
93
|
+
▼
|
|
94
|
+
是否 disableAuth === true?
|
|
95
|
+
├── 是 → 允许所有访问(调试模式)
|
|
96
|
+
│
|
|
97
|
+
▼
|
|
98
|
+
page.accessControlEnabled === false?
|
|
99
|
+
├── 是 → 跳过 SSO 认证 + 跳过权限校验
|
|
100
|
+
│
|
|
101
|
+
▼
|
|
102
|
+
是否在 noPermissionRouteList 中?
|
|
103
|
+
├── 是 → 允许访问,菜单按各项独立判断
|
|
104
|
+
│
|
|
105
|
+
▼
|
|
106
|
+
是否是非动态路由(不在 PAGES 中)?
|
|
107
|
+
├── 是 → 交给 Umi 处理(404 等静态路由)
|
|
108
|
+
│
|
|
109
|
+
▼
|
|
110
|
+
是否是超级用户?
|
|
111
|
+
├── 是 → 允许访问所有页面
|
|
112
|
+
│
|
|
113
|
+
▼
|
|
114
|
+
Tier 1: 页面在菜单中?
|
|
115
|
+
├── 是 → 沿用菜单权限逻辑:
|
|
116
|
+
│ 菜单项 adminOnly === true?
|
|
117
|
+
│ ├── 是 → 禁止访问
|
|
118
|
+
│ 检查 side_menus 白名单
|
|
119
|
+
│ ├── 匹配 → 允许访问
|
|
120
|
+
│ └── 不匹配 → 显示 403
|
|
121
|
+
│
|
|
122
|
+
▼
|
|
123
|
+
Tier 2: 隐藏页面(不在菜单中)
|
|
124
|
+
adminOnly === true?
|
|
125
|
+
├── 是 → 显示 403
|
|
126
|
+
accessControlEnabled === true?
|
|
127
|
+
├── 是 → 检查 routeKey ∈ side_menus
|
|
128
|
+
│ ├── 匹配 → 允许访问
|
|
129
|
+
│ └── 不匹配 → 显示 403
|
|
130
|
+
└── 否 → 允许访问
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## 文件清单
|
|
134
|
+
|
|
135
|
+
### 新增文件
|
|
136
|
+
|
|
137
|
+
| 文件路径 | 说明 |
|
|
138
|
+
| ------------------------- | ------------------ |
|
|
139
|
+
| `src/pages/403/index.tsx` | 403 无权限页面组件 |
|
|
140
|
+
|
|
141
|
+
### 修改文件
|
|
142
|
+
|
|
143
|
+
| 文件路径 | 修改内容 |
|
|
144
|
+
| --- | --- |
|
|
145
|
+
| `src/common/menu/types.ts` | 新增 `noPermissionRouteList` 类型定义 |
|
|
146
|
+
| `src/constants/index.ts` | 新增 `isNoPermissionRoute`、`getNoPermissionRouteList` 函数 |
|
|
147
|
+
| `src/common/menu/parser.ts` | 新增 `filterMenuItems`、`isMenuAllowed` 等权限过滤函数 |
|
|
148
|
+
| `src/layouts/index.tsx` | 新增 `isForbidden` 判断,集成 `isNoPermissionRoute` |
|
|
149
|
+
| `src/layouts/components/menu/index.tsx` | 集成菜单过滤,支持免权限校验路由 |
|
|
150
|
+
| `src/components/MicroAppLoader/index.tsx` | 集成 `isNoPermissionRoute`,免权限路由直接加载 |
|
|
151
|
+
| `src/app.tsx` | SSO 认证检查,使用 `isNoAuthRoute` |
|
|
152
|
+
| `config/routes.ts` | 新增 `/403` 路由 |
|
|
153
|
+
|
|
154
|
+
## API / 组件接口
|
|
155
|
+
|
|
156
|
+
### Window 配置
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
interface Window {
|
|
160
|
+
__MICO_CONFIG__?: {
|
|
161
|
+
/**
|
|
162
|
+
* 免认证路由列表(跳过 SSO 登录)
|
|
163
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾)
|
|
164
|
+
*/
|
|
165
|
+
noAuthRouteList?: string[];
|
|
166
|
+
/**
|
|
167
|
+
* 免权限校验路由列表(跳过菜单权限检查)
|
|
168
|
+
* 支持精确匹配和前缀匹配(以 /* 结尾)
|
|
169
|
+
* 注意:如果同时需要跳过 SSO,需同时配置 noAuthRouteList
|
|
170
|
+
*/
|
|
171
|
+
noPermissionRouteList?: string[];
|
|
172
|
+
/** 关闭权限控制(菜单全部显示,路由不校验权限) */
|
|
173
|
+
disableAuth?: boolean;
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
### 路由判断函数
|
|
179
|
+
|
|
180
|
+
```typescript
|
|
181
|
+
// 判断是否为免认证路由(跳过 SSO)
|
|
182
|
+
function isNoAuthRoute(pathname: string): boolean;
|
|
183
|
+
|
|
184
|
+
// 判断是否为免权限校验路由(跳过菜单权限检查)
|
|
185
|
+
function isNoPermissionRoute(pathname: string): boolean;
|
|
186
|
+
|
|
187
|
+
// 判断是否关闭权限控制
|
|
188
|
+
function isAuthDisabled(): boolean;
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### MenuFilterOptions
|
|
192
|
+
|
|
193
|
+
```typescript
|
|
194
|
+
interface MenuFilterOptions {
|
|
195
|
+
/** 是否是超级用户 */
|
|
196
|
+
isSuperuser?: boolean | number;
|
|
197
|
+
/** 允许访问的菜单路径列表(白名单) */
|
|
198
|
+
sideMenus?: string[];
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### filterMenuItems
|
|
203
|
+
|
|
204
|
+
```typescript
|
|
205
|
+
/**
|
|
206
|
+
* 根据权限过滤菜单项(白名单逻辑)
|
|
207
|
+
*/
|
|
208
|
+
function filterMenuItems(
|
|
209
|
+
items: MenuItem[],
|
|
210
|
+
options?: MenuFilterOptions,
|
|
211
|
+
parentPath?: string,
|
|
212
|
+
): MenuItem[];
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### 用户信息相关字段
|
|
216
|
+
|
|
217
|
+
```typescript
|
|
218
|
+
interface IUserInfo {
|
|
219
|
+
is_superuser: boolean | number;
|
|
220
|
+
/** 允许访问的菜单路径列表 */
|
|
221
|
+
side_menus: string[];
|
|
222
|
+
/** 缺失的操作权限(用于按钮级别控制,非菜单) */
|
|
223
|
+
miss_permissions: string[];
|
|
224
|
+
}
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
## 使用示例
|
|
228
|
+
|
|
229
|
+
### 配置免认证+免权限路由
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
// config/config.dev.ts
|
|
233
|
+
window.__MICO_CONFIG__ = {
|
|
234
|
+
// 免认证路由(跳过 SSO 登录)
|
|
235
|
+
noAuthRouteList: [
|
|
236
|
+
'/',
|
|
237
|
+
'/subapp',
|
|
238
|
+
'/public/*', // 前缀匹配
|
|
239
|
+
],
|
|
240
|
+
// 免权限校验路由(跳过菜单权限检查)
|
|
241
|
+
noPermissionRouteList: [
|
|
242
|
+
'/',
|
|
243
|
+
'/subapp',
|
|
244
|
+
'/settings', // 登录后的公共页面
|
|
245
|
+
],
|
|
246
|
+
disableAuth: false,
|
|
247
|
+
};
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 静态配置(代码中)
|
|
251
|
+
|
|
252
|
+
```typescript
|
|
253
|
+
// src/constants/index.ts
|
|
254
|
+
|
|
255
|
+
// 静态免认证路由(始终生效)
|
|
256
|
+
export const NO_AUTH_ROUTE_LIST: string[] = [
|
|
257
|
+
'/user/login',
|
|
258
|
+
'/user/register',
|
|
259
|
+
'/403',
|
|
260
|
+
'/404',
|
|
261
|
+
];
|
|
262
|
+
|
|
263
|
+
// 静态免权限校验路由(始终生效)
|
|
264
|
+
export const NO_PERMISSION_ROUTE_LIST: string[] = ['/403', '/404'];
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### 菜单过滤结果
|
|
268
|
+
|
|
269
|
+
```
|
|
270
|
+
原始菜单:
|
|
271
|
+
├── 工作台 ✅ 精确匹配 "工作台"
|
|
272
|
+
├── 列队管理 ✅ 前缀匹配 "列队管理.配置队列"
|
|
273
|
+
│ └── 配置队列 ✅ 精确匹配 "列队管理.配置队列"
|
|
274
|
+
├── 质量管理 ❌ 不在白名单
|
|
275
|
+
│ └── 抽样检查 ❌ 不在白名单
|
|
276
|
+
└── 权限管理 ❌ adminOnly=true,非超级用户不可见
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
### Layout 中的权限判断
|
|
280
|
+
|
|
281
|
+
```tsx
|
|
282
|
+
// layouts/index.tsx — 双层权限判断
|
|
283
|
+
const isForbidden = useMemo(() => {
|
|
284
|
+
if (isAuthDisabled()) return false;
|
|
285
|
+
if (isNoPermissionRoute(location.pathname)) return false;
|
|
286
|
+
if (!currentRoute) return false; // 非动态路由
|
|
287
|
+
if (isSuperuserUser(currentUser?.is_superuser)) return false;
|
|
288
|
+
|
|
289
|
+
// Tier 1: 菜单权限交叉引用
|
|
290
|
+
const inAllMenu = findRouteByPath(allMenuRoutes, location.pathname);
|
|
291
|
+
if (inAllMenu) {
|
|
292
|
+
return !findRouteByPath(allowedMenuRoutes, location.pathname);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Tier 2: 隐藏页面级权限
|
|
296
|
+
if (!hasWindowPages()) return false;
|
|
297
|
+
const page = findPageByPath(getWindowPages(), location.pathname);
|
|
298
|
+
if (!page) return false;
|
|
299
|
+
if (page.adminOnly) return true;
|
|
300
|
+
if (page.accessControlEnabled) {
|
|
301
|
+
const sideMenus = (currentUser?.side_menus || []) as string[];
|
|
302
|
+
return !page.routeKey || !sideMenus.includes(page.routeKey);
|
|
303
|
+
}
|
|
304
|
+
return false;
|
|
305
|
+
}, [...]);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### MicroAppLoader 中的认证判断
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
// components/MicroAppLoader/index.tsx
|
|
312
|
+
const isAuthReady =
|
|
313
|
+
isAuthDisabled() ||
|
|
314
|
+
isPageAuthFree(location.pathname) ||
|
|
315
|
+
isNoAuthRoute(location.pathname) ||
|
|
316
|
+
isNoPermissionRoute(location.pathname) ||
|
|
317
|
+
!!initialState?.currentUser;
|
|
318
|
+
|
|
319
|
+
// 未准备好时不加载子应用
|
|
320
|
+
if (!isAuthReady) {
|
|
321
|
+
return; // 继续等待
|
|
322
|
+
}
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
判断优先级:
|
|
326
|
+
1. `isAuthDisabled()` — 全局关闭权限,直接放行
|
|
327
|
+
2. `isPageAuthFree` — **PAGES 数据驱动**,页面 `accessControlEnabled === false` 时跳过认证和权限校验
|
|
328
|
+
3. `isNoAuthRoute()` — 静态配置兜底,免认证路由(PAGES 未注入时的降级保护)
|
|
329
|
+
4. `isNoPermissionRoute()` — 免权限路由(如 403/404),无需等待 currentUser
|
|
330
|
+
5. `!!initialState?.currentUser` — 已登录,有用户信息
|
|
331
|
+
|
|
332
|
+
**注意**:`accessControlEnabled === false` 同时影响三个阶段:
|
|
333
|
+
|
|
334
|
+
- SSO 认证(`app.tsx`):跳过 `ensureSsoSession()` 和 `handleAuthFailureRedirect()`
|
|
335
|
+
- 权限校验(`layouts/index.tsx`):Tier 2 隐藏页面默认放行
|
|
336
|
+
- 子应用加载(`MicroAppLoader`):不等待 currentUser 直接加载
|
|
337
|
+
|
|
338
|
+
## 设计决策
|
|
339
|
+
|
|
340
|
+
| 决策点 | 选择 | 理由 |
|
|
341
|
+
| --- | --- | --- |
|
|
342
|
+
| accessControlEnabled 统一控制 | `false` 同时跳过认证和授权 | PAGES 数据驱动,一个字段即可标记公开页面,无需重复配置 noAuthRouteList |
|
|
343
|
+
| 认证与授权分离 | 两个独立配置项 | 不同场景需要不同组合,如"需要登录但不需要权限"的个人设置页 |
|
|
344
|
+
| 权限模型 | 白名单 (`side_menus`) | 后端返回的 `side_menus` 是允许列表,比黑名单更安全 |
|
|
345
|
+
| 403 处理 | 原地渲染组件 | 保持 URL 不变,用户体验更好,便于分享链接 |
|
|
346
|
+
| 父级菜单显示 | 前缀匹配 | 子菜单有权限时,父级菜单需要作为容器显示 |
|
|
347
|
+
| 超级用户 | 跳过所有检查 | 管理员需要完整访问权限 |
|
|
348
|
+
| 免权限路由的菜单 | 按菜单项独立判断 | 每个菜单项根据自身路由是否匹配 noPermissionRouteList 独立决定显示,避免菜单随当前页面变化 |
|
|
349
|
+
| 免认证路由的子应用 | 直接加载 | 不等待 currentUser,免认证路由本身不需要登录 |
|
|
350
|
+
| 免权限路由的子应用 | 直接加载 | 不等待 currentUser,避免加载卡住 |
|
|
351
|
+
| 静态路由不受权限控制 | 默认允许访问 | 见下方"静态路由与动态路由"说明 |
|
|
352
|
+
| adminOnly 判断 | 读取菜单数据字段 | 替代硬编码菜单名称匹配,由后端数据驱动,支持多语言且无需前端维护 |
|
|
353
|
+
|
|
354
|
+
## 静态路由与动态路由
|
|
355
|
+
|
|
356
|
+
### 定义
|
|
357
|
+
|
|
358
|
+
| 类型 | 来源 | 示例 |
|
|
359
|
+
| --- | --- | --- |
|
|
360
|
+
| **静态路由** | 代码中定义 (`config/routes.ts`) | `/`, `/403`, `/404`, `/user/login` |
|
|
361
|
+
| **动态路由** | 后端页面配置 (`window.__MICO_PAGES__`,降级 `window.__MICO_MENUS__`) | `/queue-management`, `/quality-check` |
|
|
362
|
+
|
|
363
|
+
### 权限控制范围
|
|
364
|
+
|
|
365
|
+
**当前设计:权限控制仅作用于动态路由,静态路由默认允许访问。**
|
|
366
|
+
|
|
367
|
+
```
|
|
368
|
+
权限判断逻辑(isForbidden):
|
|
369
|
+
1. 检查 isAuthDisabled() → 关闭则全部允许
|
|
370
|
+
2. 检查 isNoPermissionRoute() → 在免权限列表中则允许
|
|
371
|
+
3. 检查 currentRoute(PAGES 中的路由)→ 不存在则非动态路由,交给 Umi
|
|
372
|
+
4. 检查 isSuperuserUser() → 超级用户放行
|
|
373
|
+
5. Tier 1: 在菜单中 → 检查 allowedMenuRoutes
|
|
374
|
+
6. Tier 2: 不在菜单(隐藏页面)→ 检查 adminOnly + accessControlEnabled + routeKey
|
|
375
|
+
7. 都不匹配 → 放行
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### 设计理由
|
|
379
|
+
|
|
380
|
+
1. **职责分离**:静态路由是框架层面的基础设施(登录、错误页),动态路由才是业务功能
|
|
381
|
+
2. **简化配置**:不需要额外配置哪些静态路由需要权限,减少出错概率
|
|
382
|
+
3. **向后兼容**:现有部署无需任何改动
|
|
383
|
+
4. **复杂度控制**:避免引入更多配置项增加理解成本
|
|
384
|
+
|
|
385
|
+
### 影响
|
|
386
|
+
|
|
387
|
+
当用户 `is_superuser=false` 且 `side_menus=[]` 时:
|
|
388
|
+
|
|
389
|
+
| 路由 | 结果 | 原因 |
|
|
390
|
+
| --- | --- | --- |
|
|
391
|
+
| `/` (Home) | ✅ 正常显示 | 静态路由,不受权限控制 |
|
|
392
|
+
| `/403` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
|
|
393
|
+
| `/404` | ✅ 正常显示 | 在 `noPermissionRouteList` 中 |
|
|
394
|
+
| `/user/login` | ✅ 正常显示 | 静态路由 + 在 `noAuthRouteList` 中 |
|
|
395
|
+
| `/queue-management` | ❌ 显示 403 | 动态路由,无权限 |
|
|
396
|
+
|
|
397
|
+
### 如需控制静态路由
|
|
398
|
+
|
|
399
|
+
如果业务上需要让某个静态路由(如 `/`)也受权限控制,有以下方案:
|
|
400
|
+
|
|
401
|
+
1. **方案 A**:将该路由从静态路由改为动态路由(通过菜单配置)
|
|
402
|
+
2. **方案 B**:在对应页面组件内部自行判断权限并重定向
|
|
403
|
+
3. **方案 C**:扩展 `isForbidden` 逻辑(需额外开发,当前未实现)
|
|
404
|
+
|
|
405
|
+
## 注意事项
|
|
406
|
+
|
|
407
|
+
- `noAuthRouteList` 和 `noPermissionRouteList` 是独立的,需要根据场景分别配置
|
|
408
|
+
- 两个列表都支持 `/*` 后缀进行前缀匹配
|
|
409
|
+
- `side_menus` 为空时,非超级用户没有任何菜单权限
|
|
410
|
+
- `side_menus` 格式为菜单路径,如 `"列队管理.配置队列"`
|
|
411
|
+
- `miss_permissions` 用于按钮级别权限控制,不影响菜单显示
|
|
412
|
+
- 403 页面在 Layout 内渲染,不会触发路由跳转
|
|
413
|
+
- 调试时可在控制台搜索 `isForbidden (menu check)` 或 `isForbidden (hidden page` 查看权限判断日志
|
|
414
|
+
- **常见问题**:配置了 `noAuthRouteList` 但页面仍显示 403,需同时配置 `noPermissionRouteList`
|
|
415
|
+
|
|
416
|
+
## 相关文档
|
|
417
|
+
|
|
418
|
+
- [路由与菜单解耦](./feature-路由与菜单解耦.md) - 路由注册与菜单导航数据源分离、双层权限详细说明
|
|
419
|
+
- [微前端模式](./feature-微前端模式.md) - 微应用加载机制
|
|
420
|
+
- [日志与常量](./arch-日志与常量.md) - 常量管理
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# 路由与菜单解耦
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-02-25
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
将动态路由注册与菜单导航的数据源解耦。路由注册消费 `window.__MICO_PAGES__`(页面列表),菜单栏消费 `window.__MICO_MENUS__`(菜单树)。两者独立运作,权限通过菜单交叉引用 + 页面级兜底的双层策略控制。
|
|
8
|
+
|
|
9
|
+
## 技术方案
|
|
10
|
+
|
|
11
|
+
### 技术栈
|
|
12
|
+
|
|
13
|
+
- 框架:React 18 + @umijs/max
|
|
14
|
+
- 微前端:qiankun
|
|
15
|
+
- 状态管理:Umi initialState
|
|
16
|
+
|
|
17
|
+
### 数据源关系
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
__MICO_PAGES__ (扁平列表,所有页面)
|
|
21
|
+
├── 菜单中的页面(一定能在 __MICO_MENUS__ 中找到对应菜单项)
|
|
22
|
+
└── 隐藏页面(不在菜单中显示,但路由可访问)
|
|
23
|
+
|
|
24
|
+
__MICO_MENUS__ (菜单树,用于导航)
|
|
25
|
+
└── page 字段 → 引用 __MICO_PAGES__ 中的某个页面
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### 数据流
|
|
29
|
+
|
|
30
|
+
```
|
|
31
|
+
Before:
|
|
32
|
+
patchClientRoutes ← extractRoutes(MENUS)
|
|
33
|
+
layouts 权限校验 ← extractRoutes(MENUS) + filterMenuItems(MENUS)
|
|
34
|
+
菜单渲染 ← parseMenuItems(MENUS)
|
|
35
|
+
|
|
36
|
+
After:
|
|
37
|
+
patchClientRoutes ← getDynamicRoutes() → PAGES 优先,降级 MENUS
|
|
38
|
+
layouts 权限校验 ← getDynamicRoutes() + extractRoutes(MENUS) + filterMenuItems(MENUS)
|
|
39
|
+
菜单渲染 ← parseMenuItems(MENUS) ← 不变
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### 权限控制流程
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
用户访问路由
|
|
46
|
+
│
|
|
47
|
+
▼
|
|
48
|
+
disableAuth / noPermissionRoute / superuser?
|
|
49
|
+
├── 是 → 放行
|
|
50
|
+
│
|
|
51
|
+
▼
|
|
52
|
+
page.accessControlEnabled === false?
|
|
53
|
+
├── 是 → 跳过 SSO 认证 + 跳过权限校验(公开页面)
|
|
54
|
+
│
|
|
55
|
+
▼
|
|
56
|
+
非动态路由(不在 PAGES 中)?
|
|
57
|
+
├── 是 → 交给 Umi 处理(404 等)
|
|
58
|
+
│
|
|
59
|
+
▼
|
|
60
|
+
┌─────────────────────────────────────────────┐
|
|
61
|
+
│ Tier 1: 菜单权限交叉引用 │
|
|
62
|
+
│ 页面在 __MICO_MENUS__ 中? │
|
|
63
|
+
│ ├── 是 → 沿用 sideMenus 白名单逻辑 │
|
|
64
|
+
│ │ 在 allowedMenuRoutes 中? │
|
|
65
|
+
│ │ ├── 是 → 放行 │
|
|
66
|
+
│ │ └── 否 → 403 │
|
|
67
|
+
│ └── 否 → 进入 Tier 2 │
|
|
68
|
+
├─────────────────────────────────────────────┤
|
|
69
|
+
│ Tier 2: 隐藏页面级权限 │
|
|
70
|
+
│ (仅 PAGES 数据可用时生效) │
|
|
71
|
+
│ adminOnly: true → 403 │
|
|
72
|
+
│ accessControlEnabled: true │
|
|
73
|
+
│ → routeKey ∈ sideMenus ? 放行 : 403 │
|
|
74
|
+
│ 其他 → 放行 │
|
|
75
|
+
└─────────────────────────────────────────────┘
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### 降级策略
|
|
79
|
+
|
|
80
|
+
当 `window.__MICO_PAGES__` 未注入时(如旧版部署),`getDynamicRoutes()` 自动降级到从 `window.__MICO_MENUS__` 提取路由,行为与改动前完全一致。
|
|
81
|
+
|
|
82
|
+
```
|
|
83
|
+
getDynamicRoutes():
|
|
84
|
+
hasWindowPages() ?
|
|
85
|
+
├── 是 → extractRoutesFromPages(PAGES) ← 新逻辑
|
|
86
|
+
└── 否 → extractRoutes(MENUS) ← 旧逻辑,向后兼容
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## 文件清单
|
|
90
|
+
|
|
91
|
+
### 修改文件
|
|
92
|
+
|
|
93
|
+
| 文件路径 | 修改内容 |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `src/common/menu/types.ts` | `PageConfig` 新增 `accessControlEnabled`、`routeKey` 字段;新增 `PublicPageItem` 类型(`Omit<PageConfig, ...>`);Window 声明新增 `__MICO_PAGES__` |
|
|
96
|
+
| `src/common/menu/parser.ts` | 新增 `getWindowPages`、`hasWindowPages`、`extractRoutesFromPages`、`getDynamicRoutes`、`findPageByPath` 函数;导出 `isSuperuserUser` |
|
|
97
|
+
| `src/app.tsx` | `patchClientRoutes` 改为调用 `getDynamicRoutes()` |
|
|
98
|
+
| `src/layouts/index.tsx` | 路由匹配改用 `allPageRoutes`;权限判断改为双层逻辑(菜单交叉引用 + 隐藏页面级兜底) |
|
|
99
|
+
|
|
100
|
+
### 未修改文件
|
|
101
|
+
|
|
102
|
+
| 文件路径 | 说明 |
|
|
103
|
+
| --- | --- |
|
|
104
|
+
| `src/layouts/components/menu/index.tsx` | 菜单渲染逻辑不变,继续消费 `__MICO_MENUS__` |
|
|
105
|
+
|
|
106
|
+
## API / 组件接口
|
|
107
|
+
|
|
108
|
+
### PublicPageItem
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
type PublicPageItem = Omit<
|
|
112
|
+
PageConfig,
|
|
113
|
+
'workspaceSubdomain' | 'publishedBy' | 'versions' | 'createdAt' | 'updatedAt'
|
|
114
|
+
>;
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`PageConfig` 新增字段:
|
|
118
|
+
|
|
119
|
+
| 字段 | 类型 | 说明 |
|
|
120
|
+
| --- | --- | --- |
|
|
121
|
+
| `accessControlEnabled` | `boolean` | 是否开启访问控制。`false` 时跳过 SSO 认证和权限校验(公开页面);`true` 时需要登录且通过 `routeKey` 校验权限 |
|
|
122
|
+
| `routeKey` | `string \| null` | 路由权限标识(用于匹配 sideMenus) |
|
|
123
|
+
|
|
124
|
+
### 页面数据函数
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
/** 获取 window.__MICO_PAGES__ */
|
|
128
|
+
function getWindowPages(): PublicPageItem[];
|
|
129
|
+
|
|
130
|
+
/** 判断页面数据是否可用 */
|
|
131
|
+
function hasWindowPages(): boolean;
|
|
132
|
+
|
|
133
|
+
/** 从页面数据提取路由配置 */
|
|
134
|
+
function extractRoutesFromPages(pages: PublicPageItem[]): ParsedRoute[];
|
|
135
|
+
|
|
136
|
+
/** 获取动态路由(PAGES 优先,降级 MENUS) */
|
|
137
|
+
function getDynamicRoutes(): ParsedRoute[];
|
|
138
|
+
|
|
139
|
+
/** 根据路径查找页面配置(精确匹配 + 通配符) */
|
|
140
|
+
function findPageByPath(pages: PublicPageItem[], pathname: string): PublicPageItem | undefined;
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Window 全局变量
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
interface Window {
|
|
147
|
+
/** 页面列表 — 动态路由注册的数据源 */
|
|
148
|
+
__MICO_PAGES__?: PublicPageItem[];
|
|
149
|
+
/** 菜单树 — 菜单导航的数据源 */
|
|
150
|
+
__MICO_MENUS__?: MenuItem[];
|
|
151
|
+
}
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## 设计决策
|
|
155
|
+
|
|
156
|
+
| 决策点 | 选择 | 理由 |
|
|
157
|
+
| --- | --- | --- |
|
|
158
|
+
| 路由数据源 | `__MICO_PAGES__` 独立于菜单 | 页面和菜单是不同维度:页面定义"有什么路由",菜单定义"导航怎么组织";解耦后支持隐藏页面 |
|
|
159
|
+
| 权限方案 | 菜单交叉引用 + 页面级兜底 | 菜单中的页面复用现有 sideMenus 白名单逻辑,改动最小;隐藏页面用 adminOnly + accessControlEnabled 兜底 |
|
|
160
|
+
| 降级策略 | `getDynamicRoutes()` 自动降级 | `__MICO_PAGES__` 未注入时回退到旧行为,零配置向后兼容 |
|
|
161
|
+
| PublicPageItem 定义 | `Omit<PageConfig, ...>` 派生 | 保持与 PageConfig 的类型继承关系,避免字段重复定义和类型漂移 |
|
|
162
|
+
| accessControlEnabled 语义 | `false` = 公开页面(跳过认证+授权),`true` = 需要权限检查 | 一个字段统一控制 SSO 认证、权限校验、子应用加载等待,减少配置冗余 |
|
|
163
|
+
| 隐藏页面权限 | 先 adminOnly 再 accessControlEnabled | adminOnly 是硬拦截,accessControlEnabled + routeKey 提供用户级粒度控制 |
|
|
164
|
+
|
|
165
|
+
## 已知限制与待改进
|
|
166
|
+
|
|
167
|
+
- `routeKey` 当前复用 `sideMenus` 做权限匹配,后续可能独立为专门的权限字段
|
|
168
|
+
- 隐藏页面的权限控制依赖 `routeKey` 存在于 `sideMenus` 中,需要后端在下发用户权限时包含隐藏页面的 `routeKey`
|
|
169
|
+
|
|
170
|
+
## 注意事项
|
|
171
|
+
|
|
172
|
+
- `__MICO_PAGES__` 只包含用户在后台配置的微应用页面,404/403 等静态页面不在其中
|
|
173
|
+
- `__MICO_MENUS__` 中菜单项的 `page` 字段引用的是 `__MICO_PAGES__` 中的某个页面
|
|
174
|
+
- 调试时可在控制台搜索 `isForbidden (menu check)` 或 `isForbidden (hidden page` 查看权限判断日志
|
|
175
|
+
|
|
176
|
+
## 相关文档
|
|
177
|
+
|
|
178
|
+
- [微前端模式](./feature-微前端模式.md) - 微应用加载机制
|
|
179
|
+
- [菜单权限控制](./feature-菜单权限控制.md) - sideMenus 白名单权限逻辑
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# 修复 SSO 无限重定向循环
|
|
2
|
+
|
|
3
|
+
> 创建时间:2026-03-12
|
|
4
|
+
|
|
5
|
+
## 功能概述
|
|
6
|
+
|
|
7
|
+
修复 SSO 登录流程中 `redirect_count` 防护机制被过早清除,导致 `fetchUserInfo` 返回 401 时可能触发无限 SSO 重定向循环的问题。
|
|
8
|
+
|
|
9
|
+
## 问题分析
|
|
10
|
+
|
|
11
|
+
### 原始流程(存在缺陷)
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
SSO 回跳(URL 含 redirect_count=1 & ticket)
|
|
15
|
+
→ ensureSsoSession() ticket 换 token 成功
|
|
16
|
+
→ 立即删除 redirect_count ← 问题根因
|
|
17
|
+
→ fetchUserInfo() 返回 401
|
|
18
|
+
→ refreshAuthToken() 失败
|
|
19
|
+
→ handleAuthFailureRedirect() 检查 redirect_count = 0(已被删除)
|
|
20
|
+
→ 允许重定向 → 跳转 SSO
|
|
21
|
+
→ SSO 自动登录回跳 → 重复以上流程 → 无限循环
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### 根因
|
|
25
|
+
|
|
26
|
+
`ensureSsoSession()` 在 ticket 换取 token 成功后**立即删除** URL 中的 `redirect_count` 参数,但后续的 `fetchUserInfo` 尚未执行。当 `fetchUserInfo` 返回 401 且 token 刷新失败时,`handleAuthFailureRedirect()` 因检测不到 `redirect_count` 而误判为首次重定向,触发循环。
|
|
27
|
+
|
|
28
|
+
## 技术方案
|
|
29
|
+
|
|
30
|
+
### 核心思路
|
|
31
|
+
|
|
32
|
+
将 `redirect_count` 的清除时机从"SSO ticket 换 token 成功后"延后到"整个认证流程(SSO + fetchUserInfo)完全成功后"。
|
|
33
|
+
|
|
34
|
+
### 修复后流程
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
SSO 回跳(URL 含 redirect_count=1 & ticket)
|
|
38
|
+
→ ensureSsoSession() ticket 换 token 成功
|
|
39
|
+
→ 保留 redirect_count ← 修复点
|
|
40
|
+
→ fetchUserInfo() 返回 401
|
|
41
|
+
→ refreshAuthToken() 失败
|
|
42
|
+
→ handleAuthFailureRedirect() 检查 redirect_count = 1
|
|
43
|
+
→ 已达最大重定向次数 → 停止重定向 ✅
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
```
|
|
47
|
+
SSO 回跳(URL 含 redirect_count=1 & ticket)
|
|
48
|
+
→ ensureSsoSession() ticket 换 token 成功
|
|
49
|
+
→ 保留 redirect_count
|
|
50
|
+
→ fetchUserInfo() 成功
|
|
51
|
+
→ clearRedirectCount() 清除 redirect_count ✅
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## 文件清单
|
|
55
|
+
|
|
56
|
+
### 修改文件
|
|
57
|
+
|
|
58
|
+
| 文件路径 | 修改内容 |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `src/common/request/sso.ts` | 移除 `ensureSsoSession` 中过早删除 `redirect_count` 的逻辑;新增 `clearRedirectCount()` 函数 |
|
|
61
|
+
| `src/app.tsx` | 导入 `clearRedirectCount`,在 `fetchUserInfo` 成功后调用 |
|
|
62
|
+
|
|
63
|
+
## API / 组件接口
|
|
64
|
+
|
|
65
|
+
### clearRedirectCount
|
|
66
|
+
|
|
67
|
+
```typescript
|
|
68
|
+
export const clearRedirectCount = (): void
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
清除 URL 中的 `redirect_count` 参数。仅在整个认证流程完全成功后调用。
|
|
72
|
+
|
|
73
|
+
## 设计决策
|
|
74
|
+
|
|
75
|
+
| 决策点 | 选择 | 理由 |
|
|
76
|
+
| --- | --- | --- |
|
|
77
|
+
| redirect_count 清除时机 | 延后到 fetchUserInfo 成功后 | 最小改动,不改变现有 URL 参数机制,仅调整调用时序 |
|
|
78
|
+
| clearRedirectCount 放置位置 | `sso.ts` | 与 `handleAuthFailureRedirect` 同属 SSO 模块,保持内聚 |
|
|
79
|
+
|
|
80
|
+
## 注意事项
|
|
81
|
+
|
|
82
|
+
- `handleAuthFailureRedirect` 可能在 `request/index.ts`(refresh 失败时)和 `app.tsx`(fetchUserInfo 返回 null 时)被双重调用,但修复后两处检查到 `redirect_count >= 1` 均会停止重定向,不影响正确性
|
|
83
|
+
- `redirect_count` 最大值为 1,即只允许一次自动 SSO 重定向尝试
|
|
84
|
+
|
|
85
|
+
## 相关文档
|
|
86
|
+
|
|
87
|
+
- [请求模块架构](./arch-请求模块.md)
|
|
88
|
+
- [菜单权限控制](./feature-菜单权限控制.md)
|