409 lines
27 KiB
Markdown
409 lines
27 KiB
Markdown
# Sub2API 独立业务插件框架 V1 RFC
|
||
|
||
状态:V1 通用插件控制面参考实现已落库;订阅业务插件只读适配与 Core Host Adapter/写操作仍为 Draft
|
||
|
||
本文规划一种不改动 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 已提供这些接口。
|
||
|
||
本文不是现有 `.s2plugin` 协议的直接改版。现有 `.s2plugin` 继续只负责 `openai.oauth.outbound_transport.v1`。本 RFC 的 V1 是部署级业务插件约定,不向当前 Core 增加新的路由、数据表或业务 RPC。
|
||
|
||
## 0. 约束与可行性边界
|
||
|
||
本版本必须同时满足以下约束:
|
||
|
||
- 不修改 Sub2API 的 Go、Vue、数据库迁移和现有鉴权实现;
|
||
- 不在插件内建立 Core 用户表,管理员身份以 Core 现有账号和角色为准;
|
||
- 插件后端独立监听 `PLUGIN_PORT`,由 Nginx/Caddy/Traefik 等部署层转发;
|
||
- 通过 Core 已有 `custom_menu_items` 配置添加 `visibility=admin` 的 iframe 菜单入口;
|
||
- 插件只在服务端调用 Core 现有 API,浏览器不持有 `x-api-key` 或 Core JWT;
|
||
- 第一阶段只搭框架、登录、权限、健康检查、嵌入和只读联调,不实现订阅购买写操作。
|
||
|
||
现有自定义菜单可以完成“把插件控制面显示在 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。反向代理只改变网络路径,不改变这一认证边界。
|
||
|
||
## 1. 决策摘要
|
||
|
||
### 1.1 推荐拓扑
|
||
|
||
```text
|
||
管理员浏览器
|
||
│ Core 页面中的 admin 自定义菜单
|
||
▼
|
||
Core `/custom/PLUGIN_ID`
|
||
│ 现有 sandbox iframe;只加载插件 UI,不共享 Core 存储
|
||
▼
|
||
反向代理 `/extensions/PLUGIN_ID/*`
|
||
│ 转发到独立服务 `127.0.0.1:PLUGIN_PORT`
|
||
▼
|
||
Plugin Admin 控制面(统一 TDesign Shell)
|
||
├─ `/login` 唯一登录入口
|
||
├─ 服务端调用 Core `/api/v1/auth/login`(需要时调用 `/login/2fa`)
|
||
├─ 调用 Core `/api/v1/auth/me`,确认 `role=admin`
|
||
├─ 建立唯一的插件 HttpOnly 会话,Core access/refresh token 只在服务端保存
|
||
├─ 挂载已启用业务模块(订阅等),模块不再登录
|
||
└─ 通过受控 BFF/内部服务调用业务模块和 Core Admin API
|
||
▼
|
||
Sub2API Core 现有认证、Admin API 和订阅/余额账本
|
||
```
|
||
|
||
插件后台是独立服务,不以高权限子进程形式嵌入核心,也不直接连接核心数据库。V1 通过部署层完成端口映射,通过 Core 现有登录/2FA、`/auth/me` 和 Admin API 完成功能联调;浏览器永远不接触 Admin Key,也不接触 Core JWT。插件后端不建立用户表,只维护短期会话(单实例可使用内存,多实例可使用插件自己的 Redis/会话存储)。这里的前提是插件属于受信任的内部服务:登录密码会在一次请求中经过插件后端再转交 Core,但不落库、不写日志;若要求插件进程也完全接触不到密码,需要另行引入 Core SSO/OIDC。
|
||
|
||
如果部署环境只允许服务到服务调用,也可以把 Core Admin API Key 放在插件后端 secret 中作为临时 BFF 凭据;这时所有命令都以该 Key 对应的管理员身份执行,属于单一服务身份模式,不等同于当前浏览器管理员的 SSO,也不替代 V1 的管理员登录方案。
|
||
|
||
### 1.2 V1 的默认范围
|
||
|
||
- 插件后端拥有独立服务和独立发布版本;前端由统一控制面承载。
|
||
- 只允许管理员登录;普通用户访问插件后台一律拒绝。
|
||
- 首版支持管理员登录、权限校验、健康检查、嵌入和只读订阅数据展示。
|
||
- 余额购买命令暂不实现,待框架验收后再复用现有订阅逻辑设计原子入口。
|
||
- 现有 `.s2plugin` 传输插件 ABI、路由和生命周期保持不变。
|
||
- 不实现插件任意注册核心路由、任意执行 SQL、任意读取宿主 Cookie、任意注入 Vue 路由或任意修改 Core 菜单组件。
|
||
|
||
## 2. 为什么不复用现有 `.s2plugin`
|
||
|
||
现有插件协议的能力是 `GetInfo`、`Health`、配置读写、配置测试和 `Forward` HTTP 流。它的清单只接受 `openai.oauth.outbound_transport.v1`,UI 也只是无管理员 Token 的配置 iframe。
|
||
|
||
因此它不适合安全承载以下业务:
|
||
|
||
- 管理员登录和角色授权;
|
||
- 订阅商品、余额购买和订单审计;
|
||
- 核心数据库事务或迁移;
|
||
- 用户页面、菜单、任意 HTTP 路由和支付/订阅回调。
|
||
|
||
如果把这些能力硬塞入现有协议,就会同时改变进程 ABI、权限模型、数据库边界和前端路由,反而扩大与上游的冲突面。V1 采用“外部业务插件 + Core 现有 API 适配器”作为清晰的新边界;需要 Core 新增接口的部分另列为 V1.1。
|
||
|
||
## 3. 术语和边界
|
||
|
||
| 术语 | 定义 |
|
||
|---|---|
|
||
| Core | Sub2API 主服务,拥有用户、余额、Group、Key、订阅和用量账本。 |
|
||
| 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 改动的实现范围。 |
|
||
| Plugin Session Credential | 插件自己的 HttpOnly 会话标识,不等于 Core JWT 或全局 Admin Key。 |
|
||
| Capability | 插件声明的业务能力,例如 `subscription.admin.v1`。 |
|
||
| Projection | 插件保存的只读或可重建副本,不是核心账本的权威数据。 |
|
||
|
||
## 4. 认证与权限模型
|
||
|
||
### 4.1 管理员登录
|
||
|
||
V1 采用“控制面会话 + Core 现有登录”的两层模型,不创建插件用户表;所有业务模块继承控制面会话:
|
||
|
||
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,由控制面 BFF 或受控内部转发调用模块和允许的 Core API。
|
||
|
||
```text
|
||
浏览器 -> 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 没有给自定义 iframe 提供 token handoff,因此“Core 已登录后打开 iframe 自动登录”不属于 V1。V1 允许在 iframe 内显示一次 Plugin Admin 登录页;进入订阅模块时不再追加登录。由于 sandbox/第三方 Cookie 策略可能让嵌入会话在刷新后失效,控制面必须提供“新窗口打开”入口。以后如需真正无感 SSO,另行设计一次性 code + state 或 `postMessage` 交接机制,JWT 不应放在 URL。
|
||
|
||
### 4.2 权限判定
|
||
|
||
- 默认拒绝所有普通用户、未登录用户和过期会话。
|
||
- 插件会话只包含 `plugin_id`、`admin_user_id`、角色、权限版本、签发时间和过期时间。
|
||
- V1 只定义 `plugin_admin` 角色;后续可增加 `subscription_operator`、`subscription_readonly`。
|
||
- 插件后端每次命令都携带对应 Core 管理员的 Bearer token,不使用“系统管理员”固定身份代替实际操作者(仅使用临时 Admin Key 的过渡模式除外)。
|
||
- Core 对每个现有 Admin API 操作再次做管理员角色、会话撤销和资源级授权,不把插件 UI 的按钮隐藏当作授权依据。
|
||
- 所有写操作要求 CSRF 防护、审计记录和幂等键;敏感操作可要求 step-up。
|
||
|
||
### 4.3 会话要求
|
||
|
||
- 会话 Cookie 必须 `HttpOnly`、`Secure`、`SameSite=Lax` 或更严格。
|
||
- 生产环境只允许 HTTPS;反向代理必须正确传递原始 Host 和协议。
|
||
- 登录使用短时一次性 state,绑定浏览器会话并防重放。
|
||
- 默认会话有效期 30 分钟,滑动续期上限 8 小时;登出立即撤销服务端会话。
|
||
- 独立 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 绝对禁止的做法
|
||
|
||
- 不把全局 `x-api-key` 注入浏览器。
|
||
- 不把 Admin Key 放进插件静态 JS、HTML、iframe、URL query、下载文件或前端 sourcemap。
|
||
- 不让插件 UI 直接请求 Core Admin API。
|
||
- 不把 Admin Key 写入普通业务日志、请求追踪、错误响应、数据库明文或备份导出。
|
||
- 不让插件直接连接 Core PostgreSQL、Redis 或宿主文件目录。
|
||
|
||
### 5.2 V1 凭据分级
|
||
|
||
| 环境 | 凭据 | 用途 | 约束 |
|
||
|---|---|---|---|
|
||
| 开发 | Core 管理员的临时登录凭据 | 调用 Core `/auth/login` 联调 | 只由开发者输入到 Plugin Admin 登录页,不写入代码、配置和日志。 |
|
||
| 测试 | Core 返回的 access/refresh token | 建立插件服务端会话 | 只存插件服务端会话存储,短 TTL,测试 Core 与测试管理员专用。 |
|
||
| 生产 | Core 返回的 access/refresh token | 代表实际登录的 Core 管理员调用现有 Admin API | 服务端加密保存或内存保存,按 Core TokenVersion/撤销结果失效;不回传浏览器。 |
|
||
|
||
V1 不要求新增 Plugin Access Token 服务,也不要求修改 Core。`ADMIN_API_KEY` 只作为服务到服务 BFF 的应急/测试凭据:它必须只存在插件后端 secret manager、受限环境变量或权限为 `0600` 的 secret 文件中,并由 Core Admin API 的 endpoint allowlist 限制。使用 Admin Key 时所有请求都以同一个管理员身份执行,审计粒度是服务身份级别,不得作为插件登录态下发给浏览器。
|
||
|
||
V1.1 如需在不保存 Core refresh token 的情况下运行,再设计 Core 签发的短时、限 scope Plugin Access Token;在 Core 提供该能力前,文档只把它视为未来接口。
|
||
|
||
### 5.3 凭据生命周期
|
||
|
||
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 过渡模式由运维轮换并撤销。
|
||
|
||
### 5.4 服务端防护
|
||
|
||
- Core 现有 Admin API 由自身 `adminAuth`、JWT 会话和管理员角色保护;插件后端再使用出站 allowlist,只能访问事先批准的 Core API 路径。
|
||
- 插件后端不接受任意 URL 转发,也不把 Core API 代理能力暴露给插件 UI。
|
||
- 请求设置超时、重试上限和熔断;禁止在超时后盲目重放非幂等命令。
|
||
- 日志对 `Authorization`、`x-api-key`、Cookie、session、余额和个人信息做结构化脱敏。
|
||
- 记录 `plugin_id`、操作者、scope、资源 ID、幂等键和结果,不记录 secret 原文。
|
||
- 生产插件运行在独立低权限账号或容器中,限制文件、网络、系统调用和环境变量。
|
||
|
||
## 6. Core API Adapter V1(使用现有接口)
|
||
|
||
本节描述在“不修改 Sub2API”前提下插件可以使用的最小集成面。它不是 Core 已注册的插件协议,而是插件后端对现有 HTTP API 的严格 allowlist;实现前应根据目标版本在插件配置中冻结路径和响应字段。
|
||
|
||
### 6.1 认证头
|
||
|
||
```http
|
||
Authorization: Bearer CORE_ACCESS_TOKEN
|
||
X-Request-Id: UNIQUE_REQUEST_ID
|
||
```
|
||
|
||
`CORE_ACCESS_TOKEN` 只允许由插件后端的会话适配器发送。插件浏览器、静态资源和 iframe 消息中不得出现该 token。
|
||
|
||
### 6.2 V1 允许的接口类别
|
||
|
||
```text
|
||
POST /api/v1/auth/login
|
||
POST /api/v1/auth/login/2fa
|
||
POST /api/v1/auth/refresh
|
||
POST /api/v1/auth/logout
|
||
GET /api/v1/auth/me
|
||
GET /api/v1/settings/public
|
||
GET /api/v1/admin/payment/plans
|
||
GET /api/v1/admin/subscriptions
|
||
GET /api/v1/admin/subscriptions/{id}
|
||
GET /api/v1/admin/users/{id}
|
||
GET /api/v1/admin/users/{id}/subscriptions
|
||
```
|
||
|
||
实际插件先只开放只读管理员接口;不得把路径中的 `<read-only-endpoint>` 当作通配符,部署配置必须列出具体路径、方法和分页上限。现有 Core 没有 `/api/v1/plugin-host/*` 路由,V1 不调用或宣称该路径已存在。
|
||
|
||
### 6.3 V1 写操作边界
|
||
|
||
V1 框架和第一版订阅插件不实现余额扣款、订阅续期或撤销写操作。现有 Admin API 的多个调用不应拼成一笔购买,因为这样无法保证余额、订单、订阅和审计的单事务一致性。
|
||
|
||
未来要支持写操作,必须先在 Core 中增加版本化 Host Adapter 或等价的原子命令接口(需要 Core 代码、路由、审计和幂等存储变更),再由插件接入;这属于 V1.1,不是本轮“零 Core 改动”的交付物。
|
||
|
||
### 6.4 错误和重试
|
||
|
||
| HTTP | 含义 | 插件行为 |
|
||
|---|---|---|
|
||
| `401` | 凭据无效或已撤销 | 停止重试,提示重新注册/轮换。 |
|
||
| `403` | 管理员角色、会话或资源权限不足 | 不重试,记录操作者和资源。 |
|
||
| `409` | 幂等键冲突或业务状态冲突 | 查询 operation 状态后展示最终结果。 |
|
||
| `422` | 参数或余额业务校验失败 | 展示可读错误,不重试。 |
|
||
| `429` | Core 限流 | 按 `Retry-After` 有上限地重试。 |
|
||
| `5xx` | Core 暂时故障 | 只对明确幂等命令重试,指数退避。 |
|
||
|
||
## 7. 插件注册、清单与生命周期
|
||
|
||
### 7.1 与 `.s2plugin` 分离
|
||
|
||
Business Plugin V1 不直接套用现有 `manifest.schema.json`。建议新增独立的业务插件清单,例如 `business-plugin-manifest.v1.json`:
|
||
|
||
```json
|
||
{
|
||
"schema_version": 1,
|
||
"plugin_id": "example.subscription",
|
||
"name": "Example Subscription Admin",
|
||
"version": "0.1.0",
|
||
"core_api_baseline": "sub2api-0.1.183",
|
||
"capabilities": ["subscription.admin.v1"],
|
||
"tested_core_versions": ["0.1.183"],
|
||
"backend": { "health_path": "/healthz" },
|
||
"ui": { "entrypoint": "/admin" },
|
||
"publisher": { "key_id": "publisher-key-id" }
|
||
}
|
||
```
|
||
|
||
清单只声明能力和兼容范围,不授予数据库、路由或 secret 权限。V1 由部署脚本/反向代理保存清单、校验签名和允许的 Core API 路径;当前 Core 不会读取该清单,也没有业务插件注册表。插件发布包/容器镜像仍应签名,但签名验证属于部署门禁,不应写成现有 Core 能力。
|
||
|
||
### 7.1.1 受控插件市场
|
||
|
||
`plugin-admin` 可从本地或受控 HTTPS `schema_version=1` 索引展示市场条目。市场条目至少声明 `plugin_id`、版本、Core baseline、发布者 key ID、归档 SHA-256 和归档地址;远程索引必须有 `expires_at`,索引与归档主机必须匹配精确 allowlist,禁止重定向、凭据、查询参数和私网 DNS 地址。浏览器只能提交索引中的 ID/版本,下载、验签、哈希和清单兼容性校验全部由控制面服务端执行。下载成功只进入 `disabled`(已入库、待启用),不会启动插件;管理员显式启用并通过 health/readiness 检查后才进入 `healthy`。市场安装不隐式升级已有 ID,升级仍走升级和回滚流程。
|
||
|
||
### 7.2 状态机
|
||
|
||
```text
|
||
registered -> disabled -> enabled -> draining -> disabled
|
||
│ │ │
|
||
└────── incompatible └── error
|
||
```
|
||
|
||
- `registered`:部署层已登记清单和发布者,但未启用。
|
||
- `disabled`:服务存在但不接收业务请求。
|
||
- `enabled`:健康检查通过,允许插件后端调用列入 allowlist 的 Core API。
|
||
- `draining`:停止接收新命令,等待进行中的幂等命令完成。
|
||
- `error`:健康检查或 Core API 合约校验失败,默认 fail-closed。
|
||
- `incompatible`:插件清单或 Core API 版本不兼容,不允许启用。
|
||
|
||
### 7.3 升级和回滚
|
||
|
||
- 插件版本独立于 `backend/cmd/server/VERSION` 和 `frontend/package.json`。
|
||
- 升级前先执行健康检查、现有 Core API contract test 和插件自身数据备份检查。
|
||
- 新版本不兼容时保持旧版本运行,不自动覆盖正在启用的实例。
|
||
- 停用时等待进行中的命令完成;超过 drain 超时则拒绝新命令并标记待恢复。
|
||
- 插件卸载不删除 Core 订阅、余额和审计数据。
|
||
|
||
## 8. 数据归属
|
||
|
||
### 8.1 Core 权威数据
|
||
|
||
- 用户身份和管理员授权;
|
||
- 余额账本;
|
||
- 套餐价格、额度、覆盖 Group 和购买快照;
|
||
- 用户订阅状态、期限、配额和 reserved;
|
||
- 余额扣减、订阅创建/续期、撤销和审计;
|
||
- 请求计费、额度预留、结算和幂等。
|
||
|
||
### 8.2 Plugin 可拥有的数据
|
||
|
||
- 插件管理员会话和本地角色映射;
|
||
- 插件 UI 偏好、筛选条件和缓存;
|
||
- Core API operation 的本地查询索引;
|
||
- 供应商或业务侧的非权威展示配置。
|
||
|
||
插件数据库中的订阅副本必须可删除、可重建、带来源版本和更新时间。它不作为网关放行请求的依据。
|
||
|
||
## 9. 前端和菜单集成
|
||
|
||
V1 通过现有的管理员自定义菜单嵌入,不修改 Core 前端路由:
|
||
|
||
```text
|
||
Core 设置 -> custom_menu_items
|
||
id: example.subscription
|
||
visibility: admin
|
||
url: https://PLUGIN_ORIGIN/extensions/example.subscription/
|
||
|
||
Core `/custom/example.subscription`
|
||
└── sandbox iframe -> 反向代理 -> `127.0.0.1:PLUGIN_PORT`
|
||
```
|
||
|
||
`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/*` 或受控模块 BFF,由控制面调用 Core 现有认证和管理员 API。V1 不允许插件动态注入 Core Vue 路由、修改 Core 菜单组件或覆盖全局 CSS;Plugin Admin 只挂载已安装、已启用且管理员可见的业务模块。
|
||
|
||
## 10. 威胁模型与处置
|
||
|
||
| 威胁 | V1 处置 |
|
||
|---|---|
|
||
| XSS 窃取 Admin Key | 浏览器永远没有 Admin Key;Cookie HttpOnly;CSP 和输出编码。 |
|
||
| 插件前端伪造管理员命令 | 插件后端重新验证插件会话;Core 重新验证 Bearer token、管理员角色、操作者和资源权限。 |
|
||
| 插件后端日志泄露 secret | 统一日志脱敏,禁止记录 Authorization 和完整请求。 |
|
||
| 插件服务被攻破 | 限制 Token scope、TTL、出站地址和 Core API;立即撤销凭据。 |
|
||
| 重放余额购买 | Idempotency-Key + Core 事务记录 + 请求时间/nonce。 |
|
||
| CSRF | SameSite Cookie、CSRF token、Origin/Referer 校验。 |
|
||
| SSRF | 插件出站 allowlist;Core 现有 API 不接受任意 URL。 |
|
||
| 普通用户进入后台 | 插件登录调用 Core `/auth/me` 并要求 `role=admin`;所有插件路由默认 deny。 |
|
||
| 多实例状态漂移 | Core 是权威;插件状态通过健康检查和配置版本对齐。 |
|
||
| 插件卸载误删订阅 | 插件没有删除 Core 账本的权限,卸载只撤销凭据。 |
|
||
|
||
必须明确:独立插件进程不是操作系统级沙箱,签名只证明发布者和完整性,不证明代码无漏洞。生产部署仍需要低权限运行、网络隔离和可撤销凭据。
|
||
|
||
## 11. 测试门禁
|
||
|
||
### 11.1 Core API Adapter 合约测试
|
||
|
||
- Core 登录、`/login/2fa`、`/auth/me`、access/refresh 过期和撤销;
|
||
- 普通用户、停用用户、无效会话和跨插件会话均返回 `403`;
|
||
- 只读 Admin API 的字段脱敏、分页上限和资源权限;
|
||
- Core 超时、`401/403/429/5xx`、刷新 token 和重新登录;
|
||
- V1 明确没有 `/api/v1/plugin-host/*`,测试不能把未来接口当成现有接口。
|
||
|
||
### 11.2 插件后端测试
|
||
|
||
- Core 登录代理、2FA、会话过期、登出和 CSRF;
|
||
- Core access/refresh token 不进入响应、页面、日志和异常堆栈;
|
||
- Core `401/403/429/5xx` 的处理和刷新失败后的重新登录;
|
||
- 只允许配置中列出的 Core API endpoint;
|
||
- 插件服务重启后会话失效或从插件自己的会话存储恢复,不能依赖 Core 用户表。
|
||
|
||
### 11.3 浏览器和部署测试
|
||
|
||
- 管理员在 Plugin Admin 完成一次登录后可进入订阅模块,普通用户无法登录或访问任何后台 API;
|
||
- 不同屏幕下页面无横向泄露和敏感字段;
|
||
- 浏览器 DevTools 的请求、下载和页面源中没有 Admin Key;
|
||
- HTTPS、反向代理、容器低权限、secret 文件权限和日志脱敏;
|
||
- 停用、升级、回滚、凭据撤销后所有写操作按预期失败。
|
||
|
||
## 12. 分阶段实施计划(不含本次代码)
|
||
|
||
### Phase 0:冻结部署契约
|
||
|
||
- 选择外部服务部署方式(推荐同源反向代理 + 独立服务),并冻结统一控制面模块挂载方式;
|
||
- 冻结插件端口、反向代理路径、`custom_menu_items` 字段和健康检查;
|
||
- 冻结允许调用的现有 Core API 路径、字段、分页和错误处理;
|
||
- 明确插件登录通过 Core `/auth/login`/`/login/2fa`,不创建用户表;
|
||
- 建立 Core token 会话销毁、Admin Key 过渡和日志脱敏方案。
|
||
|
||
### Phase 1:框架最小实现
|
||
|
||
- 独立服务目录、清单、发布者签名和部署兼容性检查;
|
||
- Plugin Backend 管理员登录和会话;
|
||
- Core access/refresh token 服务端会话适配器;
|
||
- 插件健康检查、启停、操作审计和同源入口;
|
||
- Core API allowlist、contract test 与本地示例插件。
|
||
|
||
### Phase 2:订阅业务模块试验
|
||
|
||
- 在统一控制面内挂载只读套餐、用户余额和订阅列表模块;
|
||
- 复用控制面管理员会话、查询缓存和审计;
|
||
- 使用测试 Core 和测试账户,不连接生产余额;
|
||
- 余额购买、续费和撤销暂不实现,等待 Core 原子接口冻结。
|
||
|
||
### Phase 3:生产准备
|
||
|
||
- 未来 Core Host Adapter/短时 Plugin Access Token(若确认需要 Core 改动);
|
||
- step-up、密钥轮换和撤销;
|
||
- 多实例、备份恢复、升级回滚和故障演练;
|
||
- 通过安全、契约、浏览器和并发门禁后再启用。
|
||
|
||
## 13. 待确认事项
|
||
|
||
以下事项在写代码前必须确定:
|
||
|
||
1. V1 的订阅插件只允许管理员操作;普通用户购买页不纳入本框架首版。
|
||
2. 管理员登录是否按本文通过 Core `/auth/login` 和 `/login/2fa` 完成;确认不创建插件用户表。
|
||
3. 插件采用独立服务端口 + 同源反向代理,还是独立 origin;需确定生产网络拓扑。
|
||
4. V1 是否允许测试环境使用服务端专用 Admin Key;生产默认不使用,除非接受单一管理员审计语义。
|
||
5. 插件是否允许保存只读缓存;无论选择何种缓存,Core 必须始终是权威来源。
|
||
6. V1.1 是否需要真正无感 SSO 和 Core Host Adapter;若需要,单独立项修改 Core。
|
||
|
||
在这些问题确认前,不应开始实现插件协议、数据库表或前端页面。
|