返回文章列表
/ 架构

商户开放 API 落地方案

API Key + HMAC-SHA256 的服务端对接方案,第一批只读订单 / 商品 / 会员。

Open APIHMAC鉴权

状态:方案设计(讨论确认阶段,尚未落地代码) 范围:为商户提供服务端对接用的开放 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") 读写。

字段:

字段类型说明
idguid主键
storeIdstring所属店铺(按此隔离,商户只能管自己的 key)
appNamestring应用名称(展示用)
apiKeystring公开标识(guid),可明文存储/传输,如 ak_live_xxx
apiSecretHashstring仅存哈希(scrypt/bcrypt);明文 secret 只在创建响应里返回一次
scopesstring(JSON)作用域数组,如 ["order:read","product:read","member:read"]
statusenumactive / disabled(吊销=置 disabled,不物理删除,保留审计)
createdAtdatetime创建时间
lastUsedAtdatetime?最近调用时间(鉴权成功时更新)
expiresAtdatetime?可选过期;到期视为 disabled

约束:一个店铺可有多把 key,便于轮换/分级。

4. 鉴权与签名流程(中间件)

新增 createMerchantApiContextMiddleware(仿 store-api-context.ts):

  1. 复用 resolveStoreCode 从路径 :store / URL / header 解析店铺 code → 查 Store。
  2. 读取请求头:
    • X-Merchant-Api-Key:公开 key
    • X-Merchant-Api-Timestamp:毫秒时间戳(防重放)
    • X-Merchant-Api-Signature:HMAC-SHA256
  3. 签名串(建议):method + "\n" + path + "\n" + timestamp + "\n" + sha256(body)。
  4. 校验顺序:
    • key 存在且 status = active
    • |now - timestamp| ≤ 300s
    • 时间戳/nonce 去重(内存 LRU + 时间窗即可,多实例再考虑共享存储)
    • HMAC 比对通过
    • 端点所需 scope 被 key 的 scopes 覆盖
  5. 通过 → storeContextStorage.run(context, next);下游 @dataContext() StoreDataContext、dataLoader、会员解析零改动。
  6. 失败统一 401(未认证)/ 403(无权限),并写审计。

5. 路由与控制器

  • 新增 apiPaths.openApi 路由组(src/store/shared/paths.ts),路径带店铺、带版本前缀:
    • GET /open-api/v1/:store/orders
    • GET /open-api/v1/:store/orders/:id
    • GET /open-api/v1/:store/products
    • GET /open-api/v1/:store/products/:id
    • GET /open-api/v1/:store/members
    • GET /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. 风险与待定项

  1. body 哈希:大请求体先 sha256(body) 再参与签名,避免中间件读全量 body。
  2. 重放去重存储:内存 LRU + 时间窗足够单实例;多实例部署需共享存储(redis)。
  3. 字段投影:订单/会员必须白名单,不能 return entity。
  4. 与现有 store-api 关系:商户 API 用新前缀,不污染内部设计器 API。

11. 落地步骤(建议顺序)

  1. 实体 StoreApiKey + mapping + 索引(storeId)。
  2. backoffice 密钥管理:控制器/服务(list / create / revoke)+ 页面 + 四语文案 + 菜单。
  3. 鉴权中间件 createMerchantApiContextMiddleware(HMAC 校验、scope 校验、审计)。
  4. merchant-api.controller.ts 只读端点(订单/商品/会员)+ 字段投影。
  5. 限流、审计落库、OpenAPI 文档。

12. 涉及文件(落地时新增/修改)

新增:

  • src/common/data/entities.ts(+ mappings.ts):StoreApiKey
  • src/store/controllers/merchant-api.ts
  • src/store/middlewares/merchant-api-context.ts
  • src/backoffice/controllers/open-api/key.controller.ts
  • src/backoffice/public/services/open-api-key-service.ts
  • src/backoffice/public/pages/open-api/(index.tsx + index.scss)
  • doc/prototypes/store/open-api-key-management.html(已完成)

修改:

  • src/store/shared/paths.ts:新增 apiPaths.openApi
  • src/common/shared/names.ts:新增商户 API header 名
  • src/backoffice/strings/strings.ts(+ 三语):新增 openApi 组
  • src/backoffice/public/pages/menu.ts:菜单项
  • src/store/controllers/index.ts:注册 MerchantApiController