通用能力 BaaS 网关。把 mjava 基座的 Client/Service 能力以 REST 暴露给外部系统(Node.js / Python / 宜搭连接器 / 低代码平台)。
客户分档:
mjava-com本身不归属任何客户档,是面向外部调用方的通用入口。暴露白名单见openspec/changes/define-customer-tiering/specs/customer-tiering/spec.mdR5。
9020/api/comPOST /api/com/{vendor}/{action},如 POST /api/com/dingtalk/user.get| Header | 说明 |
|---|---|
X-Caller-Id |
调用方 ID(宜搭权限表注册) |
X-MJ-Timestamp |
当前 Unix 毫秒时间戳;与服务端时间窗 5 分钟外拒绝 |
X-MJ-Nonce |
单次随机串(16~64 字符);服务端去重(NonceCache 防重放) |
X-MJ-Signature |
见下方签名算法 |
Content-Type |
application/json |
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 序列化后的字符串)sha256_hex("")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"}))
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-tieringspec 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 才能被调用:
ActionRegistry 中通过 register(key, handler) 注册的 handlerapplication.yml 的 com.actions.enabled 列出的 key危险动作(含 delete / remove / drop / truncate / batch + 写)即使两层白名单都通过,仍需主仓维护者在 PR 中显式 ACK 才能开启。
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) |
openspec/changes/define-customer-tiering/specs/customer-tiering/spec.md R5openspec/changes/add-mjava-com/specs/baas-gateway/ + specs/caller-registry/openspec/specs/client-service-layering/spec.md R1~R7/Users/malk/Desktop/Tech/claude/后端/CLAUDE.md