feat: complete unified plugin admin v1.1.0
Business Plugins CI / check (plugin-admin) (push) Successful in 1m42s
Business Plugins CI / check (subscription-admin) (push) Successful in 1m30s

This commit is contained in:
Qiufeng
2026-08-30 12:10:04 +08:00
parent 3c1a17f4d7
commit ada4ab3c21
69 changed files with 6681 additions and 901 deletions
+43 -32
View File
@@ -2,7 +2,9 @@
状态:V1 通用插件控制面参考实现已落库;订阅业务插件只读适配与 Core Host Adapter/写操作仍为 Draft
本文规划一种不改动 Sub2API 核心代码、数据库和现有插件 ABI 的独立业务插件框架。当前 V1 控制面参考实现位于 `plugins/plugin-admin`,它负责插件清单、签名、插件市场、下载入库、启停、升级、回滚、卸载、配置、审计和菜单注入;控制面本身不是订阅后台。每个业务插件(包括独立的 `plugins/subscription-admin`)作为可选的独立服务运行在自己的端口,通过控制面安装后再由部署层反向代理和现有“管理员可见自定义菜单”嵌入 Sub2API 页面。插件登录直接调用 Core 的现有鉴权,普通账号没有访问权限,也不复制 Core 用户表。
本文规划一种不改动 Sub2API 核心代码、数据库和现有插件 ABI 的独立业务插件框架。当前 V1 控制面参考实现位于 `plugins/plugin-admin`,它负责插件清单、签名、插件市场、下载入库、启停、升级、回滚、卸载、配置、审计和菜单注入;订阅不是第二个后台,而是安装到控制面后的业务模块。订阅后端可以作为独立服务运行在自己的端口,但浏览器端统一由 Plugin Admin Shell 承载,所有业务模块共享一次管理员登录、会话、导航和 CSRF,不创建第二个登录页或 Cookie。插件登录直接调用 Core 的现有鉴权,普通账号没有访问权限,也不复制 Core 用户表。
> **前端架构修订(2026-08-30)**:文档中“独立服务”表示部署和进程边界,不表示每个业务模块都是独立产品。订阅模块只能在插件控制面会话内访问;模块入口由插件清单的 capability/menu 声明决定,未安装或未启用时不显示。
V1 已实现范围以 `plugins/plugin-admin` 控制面和本文“当前实现范围”章节为准;本文中的订阅业务插件只是首个适配样例。Core Host Adapter、短时 Plugin Access Token、无感 SSO 和余额写操作仍是后续版本设计,不代表当前 Core 已提供这些接口。
@@ -19,7 +21,7 @@ V1 已实现范围以 `plugins/plugin-admin` 控制面和本文“当前实现
- 插件只在服务端调用 Core 现有 API,浏览器不持有 `x-api-key` 或 Core JWT;
- 第一阶段只搭框架、登录、权限、健康检查、嵌入和只读联调,不实现订阅购买写操作。
现有自定义菜单可以完成“把插件页面显示在 Sub2API 管理页面内”,但现有 iframe 使用 sandbox,且 Core 前端 JWT 保存在 `localStorage`,不会自动注入跨端口 iframe。因此在完全不改 Core 的前提下,V1 的登录方式是:插件登录页把凭据转交给插件后端,插件后端调用 Core 现有登录和二次验证接口,确认 `role=admin` 后只保留短时插件会话及服务端 Core token;这复用同一套 Core 用户和角色,不复制用户表,但不是无感知的当前页面会话共享。
现有自定义菜单可以完成“把插件控制面显示在 Sub2API 管理页面内”,但现有 iframe 使用 sandbox,且 Core 前端 JWT 保存在 `localStorage`,不会自动注入跨端口 iframe。因此在完全不改 Core 的前提下,V1 的登录方式是:Plugin Admin 登录页把凭据转交给插件后端,插件后端调用 Core 现有登录和二次验证接口,确认 `role=admin` 后只保留短时控制面会话及服务端 Core token;这复用同一套 Core 用户和角色,不复制用户表。订阅模块继承控制面会话,不得再次调用 Core 登录。
如果以后要求“已登录 Core 后打开 iframe 立即无感登录”,需要一个很小的 Core 一次性登录交接接口或前端 `postMessage` 适配;这属于 V1.1,不应通过 URL 明文传递 JWT。反向代理只改变网络路径,不改变这一认证边界。
@@ -37,12 +39,13 @@ Core `/custom/PLUGIN_ID`
反向代理 `/extensions/PLUGIN_ID/*`
│ 转发到独立服务 `127.0.0.1:PLUGIN_PORT`
▼
业务插件后台
├─ `/login` 接收管理员登录请求
Plugin Admin 控制面(统一 TDesign Shell)
├─ `/login` 唯一登录入口
├─ 服务端调用 Core `/api/v1/auth/login`(需要时调用 `/login/2fa`)
├─ 调用 Core `/api/v1/auth/me`,确认 `role=admin`
├─ 建立插件 HttpOnly 会话,Core access/refresh token 只在服务端保存
└─ 通过 Bearer Core JWT 调用现有 Core Admin API
├─ 建立唯一的插件 HttpOnly 会话,Core access/refresh token 只在服务端保存
├─ 挂载已启用业务模块(订阅等),模块不再登录
└─ 通过受控 BFF/内部服务调用业务模块和 Core Admin API
▼
Sub2API Core 现有认证、Admin API 和订阅/余额账本
```
@@ -53,7 +56,7 @@ Sub2API Core 现有认证、Admin API 和订阅/余额账本
### 1.2 V1 的默认范围
- 插件拥有独立后台和独立发布版本。
- 插件后端拥有独立服务和独立发布版本;前端由统一控制面承载。
- 只允许管理员登录;普通用户访问插件后台一律拒绝。
- 首版支持管理员登录、权限校验、健康检查、嵌入和只读订阅数据展示。
- 余额购买命令暂不实现,待框架验收后再复用现有订阅逻辑设计原子入口。
@@ -78,8 +81,8 @@ Sub2API Core 现有认证、Admin API 和订阅/余额账本
| 术语 | 定义 |
|---|---|
| Core | Sub2API 主服务,拥有用户、余额、Group、Key、订阅和用量账本。 |
| Business Plugin | 独立部署的后台服务,提供一个业务域的管理 UI 和 BFF。 |
| Plugin UI | 由 Business Plugin 提供的页面,只调用自己的后端。 |
| Business Plugin | 独立部署的后台服务或业务模块,提供一个业务域的 UI 和 BFF。 |
| Plugin UI | 由统一控制面挂载的业务模块页面,只调用模块 BFF。 |
| Plugin Backend | Business Plugin 的服务端,保存插件配置、会话和操作幂等记录。 |
| Core API Adapter | V1 插件后端对 Core 现有 REST API 的服务端客户端,只允许访问明确的认证、管理员和只读业务端点。 |
| Future Host Adapter | V1.1 以后、需要修改 Core 才能提供的版本化业务 API;不属于本次无 Core 改动的实现范围。 |
@@ -91,27 +94,27 @@ Sub2API Core 现有认证、Admin API 和订阅/余额账本
### 4.1 管理员登录
V1 采用“插件会话 + Core 现有登录”的两层模型,不创建插件用户表:
V1 采用“控制面会话 + Core 现有登录”的两层模型,不创建插件用户表;所有业务模块继承控制面会话:
1. 浏览器打开插件 `/login`,只向插件后端提交 Core 管理员凭据和必要的 2FA 信息。
1. 浏览器打开 Plugin Admin `/login`,只向控制面后端提交 Core 管理员凭据和必要的 2FA 信息。
2. 插件后端服务端调用 Core `POST /api/v1/auth/login`;需要二次验证时继续调用 `POST /api/v1/auth/login/2fa`。
3. 插件后端用返回的 Core access token 调用 `GET /api/v1/auth/me`,确认用户状态正常且 `role=admin`。
4. 插件后端建立自己的短时 HttpOnly 会话;Core access/refresh token 只保存在插件服务端的会话存储中,不回传浏览器。
5. 插件业务请求只携带插件会话 Cookie,插件后端再用对应管理员的 Core Bearer token 调用允许的 Core API。
4. 控制面建立唯一的短时 HttpOnly 会话;Core access/refresh token 只保存在控制面服务端的会话存储中,不回传浏览器。
5. 订阅等业务模块请求只携带控制面会话 Cookie,由控制面 BFF 或受控内部转发调用模块和允许的 Core API。
```text
浏览器 -> Plugin /login(插件会话 Cookie 尚未建立)
Plugin -> Core /api/v1/auth/login
Plugin -> Core /api/v1/auth/login/2fa(按 Core 返回的要求)
Plugin -> Core /api/v1/auth/me(确认 role=admin)
Plugin <- 建立 HttpOnly 插件会话
浏览器 -> Plugin /admin/*(只带插件会话 Cookie)
Plugin -> Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
浏览器 -> Plugin Admin /login(控制面会话 Cookie 尚未建立)
Plugin Admin -> Core /api/v1/auth/login
Plugin Admin -> Core /api/v1/auth/login/2fa(按 Core 返回的要求)
Plugin Admin -> Core /api/v1/auth/me(确认 role=admin)
Plugin Admin <- 建立唯一 HttpOnly 控制面会话
浏览器 -> Plugin Admin /admin/* 与 /modules/*(只带同一会话 Cookie)
Plugin Admin -> 订阅模块 BFF / Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
```
这不是独立账号,也不是把 Core 用户复制到插件;密码只用于一次 Core 登录请求,插件不落库。插件会话可使用内存存储;多实例部署时使用插件自己的 Redis/会话存储,不连接 Core 数据库。Core 继续负责密码、2FA、限流、TokenVersion、撤销和管理员角色校验。
这不是独立账号,也不是把 Core 用户复制到插件;密码只用于一次 Core 登录请求,控制面不落库。插件会话可使用内存存储;多实例部署时使用插件自己的 Redis/会话存储,不连接 Core 数据库。订阅等业务模块直接复用控制面会话,不创建模块级会话。Core 继续负责密码、2FA、限流、TokenVersion、撤销和管理员角色校验。
当前 Core 没有给自定义 iframe 提供 token handoff,因此“Core 已登录后打开 iframe 自动登录”不属于 V1。V1 允许在 iframe 内显示插件登录页;由于 sandbox/第三方 Cookie 策略可能让嵌入会话在刷新后失效,插件必须提供“新窗口打开插件”入口作为稳定登录路径。以后如需真正无感 SSO,另行设计一次性 code + state 或 `postMessage` 交接机制,JWT 不应放在 URL。
当前 Core 没有给自定义 iframe 提供 token handoff,因此“Core 已登录后打开 iframe 自动登录”不属于 V1。V1 允许在 iframe 内显示一次 Plugin Admin 登录页;进入订阅模块时不再追加登录。由于 sandbox/第三方 Cookie 策略可能让嵌入会话在刷新后失效,控制面必须提供“新窗口打开”入口。以后如需真正无感 SSO,另行设计一次性 code + state 或 `postMessage` 交接机制,JWT 不应放在 URL。
### 4.2 权限判定
@@ -131,6 +134,14 @@ Plugin -> Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
- 独立 origin 嵌入时按浏览器策略使用 `SameSite=None; Secure`,同源反代优先使用 `Lax`;必须在目标浏览器验证刷新、退出和第三方 Cookie 行为。
- 插件 UI 不把 Core JWT、Admin Key 或服务凭据写入 LocalStorage、URL、HTML、日志或错误提示。
会话有效期与插件进程生命周期相互独立。插件作为常驻 HTTP 服务运行,Core
access token 过期时由后端按需调用 `/auth/refresh` 并更新服务端会话,不需要
重启插件。只有 refresh 失败、管理员被 Core 撤销、插件会话达到空闲/绝对
TTL,或管理员主动退出时,浏览器才需要重新登录。V1 默认使用内存会话,因
此控制面进程重启会使现有插件会话失效一次;这不影响 registry 中已登记插件
的恢复。需要跨重启免登录时,使用插件自有的加密共享会话存储,不改变 Core
身份权威,也不把 refresh token 下发给浏览器。
## 5. Admin Key 与服务凭据安全
### 5.1 绝对禁止的做法
@@ -145,7 +156,7 @@ Plugin -> Core /api/v1/admin/*(只在服务端带 Bearer Core JWT)
| 环境 | 凭据 | 用途 | 约束 |
|---|---|---|---|
| 开发 | Core 管理员的临时登录凭据 | 调用 Core `/auth/login` 联调 | 只由开发者输入到插件登录页,不写入代码、配置和日志。 |
| 开发 | Core 管理员的临时登录凭据 | 调用 Core `/auth/login` 联调 | 只由开发者输入到 Plugin Admin 登录页,不写入代码、配置和日志。 |
| 测试 | Core 返回的 access/refresh token | 建立插件服务端会话 | 只存插件服务端会话存储,短 TTL,测试 Core 与测试管理员专用。 |
| 生产 | Core 返回的 access/refresh token | 代表实际登录的 Core 管理员调用现有 Admin API | 服务端加密保存或内存保存,按 Core TokenVersion/撤销结果失效;不回传浏览器。 |
@@ -155,8 +166,8 @@ V1.1 如需在不保存 Core refresh token 的情况下运行,再设计 Core
### 5.3 凭据生命周期
1. 插件后端接收管理员登录请求,但不保存密码。
2. 插件后端调用 Core 登录/2FA,保存返回 token 到服务端会话,并绑定 `admin_user_id`。
1. Plugin Admin 后端接收管理员登录请求,但不保存密码。
2. Plugin Admin 后端调用 Core 登录/2FA,保存返回 token 到唯一的服务端会话,并绑定 `admin_user_id`。
3. 每次 Core 请求都使用 TLS 和 Core Bearer token;Core 继续校验签名、TokenVersion、会话绑定和角色。
4. access token 过期时只使用对应 refresh token 调用 Core `/auth/refresh`;刷新失败就销毁插件会话并要求重新登录。
5. 登出、Core 管理员撤销会话、停用插件或发现泄露时,立即删除插件会话;Admin Key 过渡模式由运维轮换并撤销。
@@ -302,11 +313,11 @@ Core `/custom/example.subscription`
└── sandbox iframe -> 反向代理 -> `127.0.0.1:PLUGIN_PORT`
```
`visibility=admin` 只负责隐藏普通账号的菜单入口,插件后端仍必须独立鉴权。现有 iframe 的 sandbox、跨端口 origin 和 Core JWT `localStorage` 使其不会自动共享 Core 登录态;因此 iframe 首屏显示插件登录页是 V1 的预期行为。插件也应提供“新窗口打开”,便于登录后保持自身会话。
`visibility=admin` 只负责隐藏普通账号的菜单入口,Plugin Admin 后端仍必须鉴权。现有 iframe 的 sandbox、跨端口 origin 和 Core JWT `localStorage` 使其不会自动共享 Core 登录态;因此 iframe 首屏显示一次控制面登录页是 V1 的预期行为。订阅模块显示在控制面内部,不再出现独立登录页。控制面也应提供“新窗口打开”,便于登录后保持自身会话。
生产部署建议把插件外部地址挂在与 Core 相同的 HTTPS 站点下,由 Nginx/Caddy 按路径反代到独立端口;这只减少浏览器跨域问题,不改变插件必须登录和服务端调用 Core 的事实。若使用独立 origin,必须在 Core CORS 中精确加入该 origin,禁止 `*`,并仅允许必要的 `Authorization` 请求头。
插件页面只调用自己的 `/plugin-api/*`,由插件后端调用 Core 现有认证和管理员 API。V1 不允许插件动态注入主应用 Vue 路由、修改核心菜单组件或覆盖全局 CSS;主应用只提供一个受权限控制的“业务插件”入口,插件内部菜单由插件自己管理。
插件模块页面只调用控制面提供的 `/plugin-api/*` 或受控模块 BFF,由控制面调用 Core 现有认证和管理员 API。V1 不允许插件动态注入 Core Vue 路由、修改 Core 菜单组件或覆盖全局 CSS;Plugin Admin 只挂载已安装、已启用且管理员可见的业务模块。
## 10. 威胁模型与处置
@@ -345,7 +356,7 @@ Core `/custom/example.subscription`
### 11.3 浏览器和部署测试
- 管理员可登录,普通用户无法登录或访问任何后台 API;
- 管理员在 Plugin Admin 完成一次登录后可进入订阅模块,普通用户无法登录或访问任何后台 API;
- 不同屏幕下页面无横向泄露和敏感字段;
- 浏览器 DevTools 的请求、下载和页面源中没有 Admin Key;
- HTTPS、反向代理、容器低权限、secret 文件权限和日志脱敏;
@@ -355,7 +366,7 @@ Core `/custom/example.subscription`
### Phase 0:冻结部署契约
- 选择外部服务部署方式(推荐同源反向代理 + 独立服务);
- 选择外部服务部署方式(推荐同源反向代理 + 独立服务),并冻结统一控制面模块挂载方式;
- 冻结插件端口、反向代理路径、`custom_menu_items` 字段和健康检查;
- 冻结允许调用的现有 Core API 路径、字段、分页和错误处理;
- 明确插件登录通过 Core `/auth/login`/`/login/2fa`,不创建用户表;
@@ -369,10 +380,10 @@ Core `/custom/example.subscription`
- 插件健康检查、启停、操作审计和同源入口;
- Core API allowlist、contract test 与本地示例插件。
### Phase 2:订阅插件试验
### Phase 2:订阅业务模块试验
- 只读套餐、用户余额和订阅列表;
- 管理员操作页面、查询缓存和审计;
- 在统一控制面内挂载只读套餐、用户余额和订阅列表模块;
- 复用控制面管理员会话、查询缓存和审计;
- 使用测试 Core 和测试账户,不连接生产余额;
- 余额购买、续费和撤销暂不实现,等待 Core 原子接口冻结。