状态:方案设计(讨论确认阶段,尚未落地代码) 范围:为商户提供服务端对接用的开放 API(第一批只读:订单 / 商品 / 会员) 原型页:
doc/prototypes/store/open-api-key-management.html
1. 目标与边界
为商户提供服务端到服务端的开放 API,使其自有系统(ERP / BI / 运维脚本)能安全读取本店数据。
本期边界(已与产品确认):
- 鉴权:API Key + HMAC-SHA256 签名(防篡改、防重放)。
- 端点范围:只读 —— 订单、商品、会员(优惠券只读可顺带纳入,成本低)。
- 凭证存储:mall 全局表(
StoreApiKey与Store同库,按storeId索引)。
非目标(本期不做,但设计预留):写操作(下单/改库存)、OAuth2 第三方授权、出站 Webhook、正式开放平台门户。
2. 现有可复用设施(避免重造)
| 能力 | 现有实现 | 复用方式 |
|---|---|---|
| 租户识别(按店铺 code) | src/store/middlewares/store-api-context.ts(resolveStoreCode 从 :store 参数 / URL / header 解析) | 商户 API 中间件直接复用店铺解析逻辑 |
| 多租户隔离 | StoreDataContext(store-data-context.ts 的 withCurrentStoreScope)自动给查询 AND storeId | 商户控制器用 @dataContext() StoreDataContext,查询天然隔离 |
| 全局数据上下文 | usingDataContext("mall", ...)(src/common/data/data-context) | StoreApiKey 落 mall 库,按此读写 |
| 令牌原语 | portal 的 TokenManager / token_data(不透明 guid + TTL + 缓存) | 仅作参考;商户用独立的 API Key 体系,不复用登录 token |
| 路由集中 | apiPaths(src/store/shared/paths.ts) | 新增 apiPaths.openApi 组,禁止硬编码 |
| 头部约定 | headerNames(src/common/shared/names.ts) | 新增商户 API 相关 header 名常量 |
3. 凭证模型(StoreApiKey,mall 全局表)
落点与 Store 实体同款:src/common/data/entities.ts 定义 + mappings.ts 注册,经 usingDataContext("mall") 读写。
字段:
| 字段 | 类型 | 说明 |
|---|---|---|
id | guid | 主键 |
storeId | string | 所属店铺(按此隔离,商户只能管自己的 key) |
appName | string | 应用名称(展示用) |
apiKey | string | 公开标识(guid),可明文存储/传输,如 ak_live_xxx |
apiSecretHash | string | 仅存哈希(scrypt/bcrypt);明文 secret 只在创建响应里返回一次 |
scopes | string(JSON) | 作用域数组,如 ["order:read","product:read","member:read"] |
status | enum | active / disabled(吊销=置 disabled,不物理删除,保留审计) |
createdAt | datetime | 创建时间 |
lastUsedAt | datetime? | 最近调用时间(鉴权成功时更新) |
expiresAt | datetime? | 可选过期;到期视为 disabled |
约束:一个店铺可有多把 key,便于轮换/分级。
4. 鉴权与签名流程(中间件)
新增 createMerchantApiContextMiddleware(仿 store-api-context.ts):
- 复用
resolveStoreCode从路径:store/ URL / header 解析店铺 code → 查Store。 - 读取请求头:
X-Merchant-Api-Key:公开 keyX-Merchant-Api-Timestamp:毫秒时间戳(防重放)X-Merchant-Api-Signature:HMAC-SHA256
- 签名串(建议):
method + "\n" + path + "\n" + timestamp + "\n" + sha256(body)。 - 校验顺序:
- key 存在且
status = active |now - timestamp| ≤ 300s- 时间戳/nonce 去重(内存 LRU + 时间窗即可,多实例再考虑共享存储)
- HMAC 比对通过
- 端点所需 scope 被 key 的
scopes覆盖
- key 存在且
- 通过 →
storeContextStorage.run(context, next);下游@dataContext() StoreDataContext、dataLoader、会员解析零改动。 - 失败统一
401(未认证)/403(无权限),并写审计。
5. 路由与控制器
- 新增
apiPaths.openApi路由组(src/store/shared/paths.ts),路径带店铺、带版本前缀:GET /open-api/v1/:store/ordersGET /open-api/v1/:store/orders/:idGET /open-api/v1/:store/productsGET /open-api/v1/:store/products/:idGET /open-api/v1/:store/membersGET /open-api/v1/:store/members/:id
- 也可复用现有
/store/:store/...挂载约定,保持租户识别一致。 - 新增控制器
src/store/controllers/merchant-api.ts,由controllers/index.ts导出注册。 - 与内部设计器用的
store-api.controller.ts互不污染(前缀不同)。
6. 端点面(只读第一批)
所有端点 @dataContext() StoreDataContext → 自动 storeId 隔离。
- 订单:
list(分页 + 状态过滤)、detail - 商品:
list、detail - 会员:
list、detail - 统一分页与字段投影白名单:订单/会员不得返回 password / 凭证类内部字段(显式投影,不
return entity)。 - 优惠券只读可一并提供
list。
7. 后台密钥管理页(原型已完成)
页面设计见 doc/prototypes/store/open-api-key-management.html,要点:
- 入口:
商户后台 / 设置 / 开放 API,当前店铺作用域(storeId隔离)。 - 说明区:Base URL、版本、鉴权方式、可折叠接入文档(curl 签名示例)。
- 工具栏:状态筛选 + 名称搜索 + 「创建密钥」。
- 表格:应用名称 / API Key(脱敏+复制)/ 作用域(Tag) / 状态 / 创建时间 / 最近使用 / 操作(查看、吊销
Popconfirm)。 - 创建抽屉:表单(应用名必填、作用域多选、可选过期)→ 成功后切一次性结果态(黄底警告 +
apiSecret/apiKey明文 + 复制)。 - 查看抽屉:只读,不返回 apiSecret。
- 吊销 = 置
disabled,二次确认。 - 全部文案走
strings.openApi.*(四语),BEM 类名open-api__*。
实现时还需要:backoffice 控制器/服务(list / create / revoke)、apiPaths.openApi、四语 openApi 组文案。
8. 横切能力(设计预留,本批可只留接口)
- 限流:按 apiKey 令牌桶(内存或 redis)。
- 审计日志:
storeId / apiKey / endpoint / time / status。 - Webhook:本批不做;签名方案一致,后续
order.created等出站事件用同一apiSecret签名即可扩展。 - 文档:NestJS 接 Swagger(
@ApiTags已具备)。
9. 安全要点清单
- secret 仅存哈希;apiKey 可明文。
- 强制 HTTPS(签名防篡改不防嗅探)。
- 防重放:时间戳窗口 + nonce 去重。
- scope 最小权限。
- 密钥轮换:支持多 key、可吊销。
- 全量审计。
- 字段脱敏:订单/会员显式投影白名单。
10. 风险与待定项
- body 哈希:大请求体先
sha256(body)再参与签名,避免中间件读全量 body。 - 重放去重存储:内存 LRU + 时间窗足够单实例;多实例部署需共享存储(redis)。
- 字段投影:订单/会员必须白名单,不能
return entity。 - 与现有 store-api 关系:商户 API 用新前缀,不污染内部设计器 API。
11. 落地步骤(建议顺序)
- 实体
StoreApiKey+ mapping + 索引(storeId)。 - backoffice 密钥管理:控制器/服务(
list / create / revoke)+ 页面 + 四语文案 + 菜单。 - 鉴权中间件
createMerchantApiContextMiddleware(HMAC 校验、scope 校验、审计)。 merchant-api.controller.ts只读端点(订单/商品/会员)+ 字段投影。- 限流、审计落库、OpenAPI 文档。
12. 涉及文件(落地时新增/修改)
新增:
src/common/data/entities.ts(+mappings.ts):StoreApiKeysrc/store/controllers/merchant-api.tssrc/store/middlewares/merchant-api-context.tssrc/backoffice/controllers/open-api/key.controller.tssrc/backoffice/public/services/open-api-key-service.tssrc/backoffice/public/pages/open-api/(index.tsx + index.scss)doc/prototypes/store/open-api-key-management.html(已完成)
修改:
src/store/shared/paths.ts:新增apiPaths.openApisrc/common/shared/names.ts:新增商户 API header 名src/backoffice/strings/strings.ts(+ 三语):新增openApi组src/backoffice/public/pages/menu.ts:菜单项src/store/controllers/index.ts:注册MerchantApiController