README.md 6.2 KB

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

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

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.ymlcom.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