# mjava-com 通用能力 BaaS 网关。把 mjava 基座的 Client/Service 能力以 REST 暴露给外部系统(Node.js / Python / 宜搭连接器 / 低代码平台)。 > 客户分档:`mjava-com` 本身不归属任何客户档,是面向外部调用方的通用入口。暴露白名单见 `openspec/changes/define-customer-tiering/specs/customer-tiering/spec.md` R5。 ## 端口与上下文 - 端口:`9020` - context-path:`/api/com` - 入口:`POST /api/com/{vendor}/{action}`,如 `POST /api/com/dingtalk/user.get` ## 调用协议 ### Headers(必填) | Header | 说明 | |---|---| | `X-Caller-Id` | 调用方 ID(宜搭权限表注册) | | `X-MJ-Timestamp` | 当前 Unix 毫秒时间戳;与服务端时间窗 5 分钟外拒绝 | | `X-MJ-Nonce` | 单次随机串(16~64 字符);服务端去重(NonceCache 防重放) | | `X-MJ-Signature` | 见下方签名算法 | | `Content-Type` | `application/json` | ### Body JSON 对象,作为 action 的入参;无入参传 `{}`。 ### 签名算法 ``` content = timestamp + "\n" + nonce + "\n" + METHOD + "\n" + path + "\n" + sha256_hex(body) signature = hex(HMAC_SHA256(secret, content)) ``` - `METHOD` 大写(`POST`) - `path` 是 servlet path(**含** context-path,如 `/api/com/dingtalk/user.get`) - `body` 是原始请求体字节(与发出的字节一致;JSON 序列化后的字符串) - 空 body 时 `sha256_hex("")` ## 调用方样例 ### Python ```python import hashlib import hmac import json import time import uuid import requests CALLER_ID = "your-caller-id" SECRET = "your-caller-secret" # 由 mjava-com 运维分配 BASE_URL = "https://your-host/api/com" def call(vendor: str, action: str, body: dict) -> dict: path = f"/api/com/{vendor}/{action}" body_str = json.dumps(body, separators=(",", ":"), ensure_ascii=False) ts = str(int(time.time() * 1000)) nonce = uuid.uuid4().hex body_hash = hashlib.sha256(body_str.encode("utf-8")).hexdigest() content = "\n".join([ts, nonce, "POST", path, body_hash]) signature = hmac.new(SECRET.encode("utf-8"), content.encode("utf-8"), hashlib.sha256).hexdigest() headers = { "Content-Type": "application/json", "X-Caller-Id": CALLER_ID, "X-MJ-Timestamp": ts, "X-MJ-Nonce": nonce, "X-MJ-Signature": signature, } r = requests.post(BASE_URL + path[len("/api/com"):], data=body_str.encode("utf-8"), headers=headers, timeout=10) r.raise_for_status() return r.json() # 示例 print(call("dingtalk", "user.get", {"userid": "U123"})) ``` ### Node.js ```javascript const crypto = require("crypto"); const axios = require("axios"); const CALLER_ID = "your-caller-id"; const SECRET = "your-caller-secret"; const BASE_URL = "https://your-host/api/com"; async function call(vendor, action, body) { const path = `/api/com/${vendor}/${action}`; const bodyStr = JSON.stringify(body); const ts = String(Date.now()); const nonce = crypto.randomBytes(16).toString("hex"); const bodyHash = crypto.createHash("sha256").update(bodyStr).digest("hex"); const content = [ts, nonce, "POST", path, bodyHash].join("\n"); const signature = crypto.createHmac("sha256", SECRET).update(content).digest("hex"); const { data } = await axios.post(`${BASE_URL}/${vendor}/${action}`, bodyStr, { headers: { "Content-Type": "application/json", "X-Caller-Id": CALLER_ID, "X-MJ-Timestamp": ts, "X-MJ-Nonce": nonce, "X-MJ-Signature": signature, }, }); return data; } call("dingtalk", "user.get", { userid: "U123" }).then(console.log); ``` ## 错误码 | code | HTTP | 含义 | |---|---|---| | `AUTH_HEADER_MISSING` | 401 | 4 个必填 Header 任一缺失 | | `AUTH_TIMESTAMP_EXPIRED` | 401 | timestamp 超出 5 分钟窗口 | | `AUTH_CALLER_NOT_FOUND` | 401 | callerId 未注册或已停用 | | `AUTH_NONCE_REPLAYED` | 401 | nonce 重复(防重放命中) | | `AUTH_SIGNATURE_INVALID` | 403 | 签名校验失败 | | `RATE_LIMITED` | 429 | 超出 caller 限流(默认 60 QPS) | | `ACTION_NOT_FOUND` | 404 | `{vendor}.{action}` 未在 `ActionRegistry` 注册 | | `ACTION_FORBIDDEN` | 403 | action 未在 yml 全局白名单或 caller 的 allowedActions | | `VENDOR_ERROR` | 500 | 第三方调用失败 | ## 调用方注册 > 标准注册流程见 `define-customer-tiering` spec R5。 调用方在宜搭权限表登记(formUuid 配在 `application.yml` `com.caller.registry.formUuid`): | 字段 | 必填 | 说明 | |---|---|---| | `callerId` | ✅ | 全局唯一 | | `callerSecret` | ✅ | HMAC 密钥(宜搭加密字段) | | `allowedActions` | ✅ | 允许调用的 `{vendor}.{action}` 数组(`*` 表全开放,慎用) | | `rateLimit` | 可选 | QPS(默认 60) | | `expireAt` | 可选 | 失效时间戳;过期自动拒绝 | | `enabled` | ✅ | false 拒绝该 caller | 热加载:`POST /_admin/reloadCaller`(dev profile)或等下次 `@Scheduled` 刷新(默认 TTL 配在 `application.yml`)。 ## 暴露白名单 只有同时满足两个白名单的 action 才能被调用: 1. **代码层**:`ActionRegistry` 中通过 `register(key, handler)` 注册的 handler 2. **配置层**:`application.yml` 的 `com.actions.enabled` 列出的 key 危险动作(含 `delete` / `remove` / `drop` / `truncate` / `batch + 写`)即使两层白名单都通过,仍需主仓维护者在 PR 中显式 ACK 才能开启。 ## 限流策略 - 默认每 caller 60 QPS(本地 `Guava RateLimiter`,进程级,**不跨实例**) - 在 `application.yml` 按 caller 覆盖:`com.rateLimit.{callerId}: {qps}` - 超出返 `429 RATE_LIMITED` ## 配置文件 | 文件 | 说明 | |---|---| | `application.yml` | 通用配置 | | `application-dev.yml.example` | dev profile 占位(含 com.caller.registry.* / com.actions.enabled 示例) | | `application-prod.yml.example` | prod profile 占位 | | `application-{profile}.yml` | 实际配置(**不入 git**) | ## 关联文档 - 客户分档 spec:`openspec/changes/define-customer-tiering/specs/customer-tiering/spec.md` R5 - BaaS 网关 capability:`openspec/changes/add-mjava-com/specs/baas-gateway/` + `specs/caller-registry/` - 基座 Client/Service 分层:`openspec/specs/client-service-layering/spec.md` R1~R7 - 后端开发主规范:`/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md`