24 KiB
Sub2API 独立业务插件框架 V1 RFC
状态:V1 通用插件控制面参考实现已落库;订阅业务插件只读适配与 Core Host Adapter/写操作仍为 Draft
本文规划一种不改动 Sub2API 核心代码、数据库和现有插件 ABI 的独立业务插件框架。当前 V1 控制面参考实现位于 plugins/plugin-admin,它负责插件清单、签名、安装、启停、升级、回滚、卸载、配置、审计和菜单注入;控制面本身不是订阅后台。每个业务插件(包括独立的 plugins/subscription-admin)作为可选的独立服务运行在自己的端口,通过控制面安装后再由部署层反向代理和现有“管理员可见自定义菜单”嵌入 Sub2API 页面。插件登录直接调用 Core 的现有鉴权,普通账号没有访问权限,也不复制 Core 用户表。
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 的登录方式是:插件登录页把凭据转交给插件后端,插件后端调用 Core 现有登录和二次验证接口,确认 role=admin 后只保留短时插件会话及服务端 Core token;这复用同一套 Core 用户和角色,不复制用户表,但不是无感知的当前页面会话共享。
如果以后要求“已登录 Core 后打开 iframe 立即无感登录”,需要一个很小的 Core 一次性登录交接接口或前端 postMessage 适配;这属于 V1.1,不应通过 URL 明文传递 JWT。反向代理只改变网络路径,不改变这一认证边界。
1. 决策摘要
1.1 推荐拓扑
管理员浏览器
│ Core 页面中的 admin 自定义菜单
▼
Core `/custom/PLUGIN_ID`
│ 现有 sandbox iframe;只加载插件 UI,不共享 Core 存储
▼
反向代理 `/extensions/PLUGIN_ID/*`
│ 转发到独立服务 `127.0.0.1:PLUGIN_PORT`
▼
业务插件后台
├─ `/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
▼
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 | 由 Business Plugin 提供的页面,只调用自己的后端。 |
| 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 现有登录”的两层模型,不创建插件用户表:
- 浏览器打开插件
/login,只向插件后端提交 Core 管理员凭据和必要的 2FA 信息。 - 插件后端服务端调用 Core
POST /api/v1/auth/login;需要二次验证时继续调用POST /api/v1/auth/login/2fa。 - 插件后端用返回的 Core access token 调用
GET /api/v1/auth/me,确认用户状态正常且role=admin。 - 插件后端建立自己的短时 HttpOnly 会话;Core access/refresh token 只保存在插件服务端的会话存储中,不回传浏览器。
- 插件业务请求只携带插件会话 Cookie,插件后端再用对应管理员的 Core Bearer token 调用允许的 Core API。
浏览器 -> 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)
这不是独立账号,也不是把 Core 用户复制到插件;密码只用于一次 Core 登录请求,插件不落库。插件会话可使用内存存储;多实例部署时使用插件自己的 Redis/会话存储,不连接 Core 数据库。Core 继续负责密码、2FA、限流、TokenVersion、撤销和管理员角色校验。
当前 Core 没有给自定义 iframe 提供 token handoff,因此“Core 已登录后打开 iframe 自动登录”不属于 V1。V1 允许在 iframe 内显示插件登录页;由于 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、日志或错误提示。
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 联调 |
只由开发者输入到插件登录页,不写入代码、配置和日志。 |
| 测试 | 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 凭据生命周期
- 插件后端接收管理员登录请求,但不保存密码。
- 插件后端调用 Core 登录/2FA,保存返回 token 到服务端会话,并绑定
admin_user_id。 - 每次 Core 请求都使用 TLS 和 Core Bearer token;Core 继续校验签名、TokenVersion、会话绑定和角色。
- access token 过期时只使用对应 refresh token 调用 Core
/auth/refresh;刷新失败就销毁插件会话并要求重新登录。 - 登出、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 认证头
Authorization: Bearer CORE_ACCESS_TOKEN
X-Request-Id: UNIQUE_REQUEST_ID
CORE_ACCESS_TOKEN 只允许由插件后端的会话适配器发送。插件浏览器、静态资源和 iframe 消息中不得出现该 token。
6.2 V1 允许的接口类别
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:
{
"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.2 状态机
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 前端路由:
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 只负责隐藏普通账号的菜单入口,插件后端仍必须独立鉴权。现有 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;主应用只提供一个受权限控制的“业务插件”入口,插件内部菜单由插件自己管理。
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 浏览器和部署测试
- 管理员可登录,普通用户无法登录或访问任何后台 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. 待确认事项
以下事项在写代码前必须确定:
- V1 的订阅插件只允许管理员操作;普通用户购买页不纳入本框架首版。
- 管理员登录是否按本文通过 Core
/auth/login和/login/2fa完成;确认不创建插件用户表。 - 插件采用独立服务端口 + 同源反向代理,还是独立 origin;需确定生产网络拓扑。
- V1 是否允许测试环境使用服务端专用 Admin Key;生产默认不使用,除非接受单一管理员审计语义。
- 插件是否允许保存只读缓存;无论选择何种缓存,Core 必须始终是权威来源。
- V1.1 是否需要真正无感 SSO 和 Core Host Adapter;若需要,单独立项修改 Core。
在这些问题确认前,不应开始实现插件协议、数据库表或前端页面。