|
|
@@ -0,0 +1,175 @@
|
|
|
+# 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`
|