Engage OpenAPI 是一个多方(租户、平台、应用 A、应用 B)参与的体系。

# OpenAPI 基本规范

  • 使用 HTTPS(TLS)对传输层加密,证书必须是 Public CA 签发的,不能是私有证书。应用服务端务必保证证书链的完整性(即完成 cURL 测试)。
  • 额外使用 AES + HMAC 进行端到端(End-to-End)的加密、签名,避免内网攻击
  • 所有接口都是 POST 请求,并以 JSON 格式进行请求与响应,哪怕是文件上传(可以使用 base64 转换)
  • 加密前的请求参数为固定格式:{"profileId": "", "userId": "", "data": {}}。data 中是实质业务参数。profileId、userId 用于表示当前用户所在租户、当前登录用户。
  • 加密前的响应参数为固定格式:{"errorCode": 0, "errorMessage": "", "errorDetail", "", "errorLink": "", "traceId": "", "data": {}}

# 请求、响应加密、签名流程

  • 请求端
    • 对请求参数({profileId: , userId: , data: })进行 JSON.stringify 并使用 UTF8 编码转为二进制,得到明文 cleartext
    • 使用 AES 算法(aes-256-cbc with pkcs#7 padding)对 cleartext 进行加密,加密 Key 为 Client Secret,得到 ciphertext(base64)。注意,这里需要随机生成一个 16 位字符串的 IV 用于 CBC 加密,并将其放到 ciphertext 的头部。
    • 获取当前时间 timestamp(秒),并生成一个随机数 nonce(最大长度 8 位,纯数字)
    • 将 ciphertext 与 nonce、timestamp 做字符串连接(使用 & 拼接字符串,注意严格按照此顺序),得到 ciphertext2
    • 对 ciphertext2 使用 HMAC 获取签名,Key 为 Client Sign,得到签名 signature(hex)。
    • 调用接口时,URL 中带上参数 ?client_id=xxx&timestamp=123&nonce=123&signature=&method=,Request Body 为 {"ciphertext": ciphertext}
  • 响应端
    • 接收参数时,首先检查 timestamp 是否在 5 分钟以内。超过 5 分钟,则有可能是重放攻击,报 400 错
    • 接着检查 signature 是否在 5 分钟内已经接收过,如果有,则可能是重放攻击,报 400 错
    • 根据 client_id 拿到 Client Sign,对请求内容进行验签。验签失败则报 401 错误
    • 根据 client_id 拿到 Client Secret,对请求内容进行解密。解密失败则报 401 错误
    • 执行业务逻辑,获得明文响应内容,如 {"errorCode": 0, "data": {}}
    • 同样生成 timestamp、nonce,并对明文响应内容进行加密、签名,加密响应内容为:{"method": "", "timestamp": 111, "nonce": 1222, "signature": "", "ciphertext": xxx}
    • 注意,如果 client_id 不存在,则会直接返回明文响应内容,如 {"errorCode": 404, "errorMessage": "not found client_id", "data": null}
  • 请求端
    • 请求端对响应内容也要做重放攻击检测、验签、解密
  • 以上 method 参数固定为 ENGAGE1-AES-HMAC

示例参考下方。

# App 创建与发布

  • App 在创建时,会分配 Client Id、Client Secret、Client Sign
  • App 需要提供一个 OpenAPI 的服务器地址,如 https://app1.com/open-api
  • App 还需要提供一个白名单 IP 列表
  • 发布 App 时,平台会与 App 做识别校验。
    • 平台首先生成一个 32 位的随机字符串(UUID)。
    • 平台以 POST 方式调用 App 的识别接口(https://app1.com/open-api/v1/identify
    • App 验签解密得到明文,然后对明文做一次翻转,加密签名后返回。
    • 平台收到响应后,验签解密,判断是否有翻转。如果都没问题,则识别成功,继而发布成功。
  • 平台会不定期进行签名检查,防止 App 不做签名检查。

# 租户购买 App

租户购买 App 后,App 才可以访问租户的数据(包括组织架构、租户其他 App 的 OpenAPI)。由于租户是在平台上进行应用购买,购买时应用还需要对租户做一些初始化工作,所以这里不采用 OAuth2 协议,而是简化处理:

  • 注意,必须是租户管理员才能购买 App。
  • 租户同意授权后,平台会携带租户 Id、名称、购买时长等信息,调用 App 的接口 https://app1.com/open-api/v1/profile/bind
  • App 执行租户的初始化工作
  • 如果没有错误,则购买成功,由平台提示将哪个租户授权给了哪个 App,然后关闭弹出框。

# App 访问其他 App 的 OpenAPI

  • 每个 App 提供的 OpenAPI 其实都是为平台增加的功能,即这些 OpenAPI 对调用方来说都像是平台提供的服务。App 不会直接调用目标 App 的 OpenAPI,而是先调用到平台,平台会在接受请求后会进行转发。
  • App 调用时,需要在目标 OpenAPI 的 URL 的 path 里需要增加 /<Target App Name> 前缀。假设平台 OpenAPI 网关地址为 https://engage.com/open-api,应用 B 的 server-addresshttps://b.com/open-api,且其提供了一个 /v1/query OpenAPI。那么应用 A 在调用时,完整地址是 https://engage.com/open-api/b/v1/query。Platform 在收到后,将 URL 去掉网关地址部分。余下的第一部分表示目标应用(/b)。继而取出目标应用的 server-address,组装完整 URL,此时就不需要在路径里加应用名了。因此转发到应用 B 的完整请求 URL 是 https://b.com/open-api/v1/query
  • 未加密的请求参数里要带上 profileId。如果有涉及到登录员工,还要带上 userId
  • 平台会检查租户与 App 的关系。没有问题,则转发到目标 App

# 性能

以上使用端到端的加密、签名方案,端到端的安全性有了保障。但在性能上可能有所损耗。解决办法有以下:

  1. 限制请求大小,如 100KB。
  2. 使用独立的加解密、签名校验服务,比如利用 Nginx、线程或其他高性能语言来集成
  3. 接口可以自己申明无需加密,只做签名(未定)
  4. 接口可以自己申明只对部分字段做加密与签名(未定)

# OpenAPI 加密、签名示例

假设 App 的 Client Id=6z2W0hljxBCK2MesrqmFE4pm7Xq0uvVX、Client Secret=Ub57FEtXQIYVrwOsWcYYAMSPItwyxWf9、Client Sign=Cb4kWhZzXRhDzA4pbJqLSfdlFjzLQdld,并假设入参为:

{
    "profileId": "egrPFiDckSs2er8uWyr9rK0dG4Li0082",
    "userId": "",
    "data": {
        "tree": true
    }
}
  1. 使用 JSON.stringify 得到明文 cleartext:
    cleartext={"profileId":"egrPFiDckSs2er8uWyr9rK0dG4Li0082","userId":"","data":{"tree":true}}
    
  2. 随机生成 16 位的 iv 向量,并使用 AES 加密得到 ciphertext,并在其头部拼接 iv
    iv=ed932439a666f716
    
    ciphertext=t9nWfafTcRDHv0KoD/+1t46H7vJ2aYhdXEUAcb+Eqh22whj9w2kO7vHx1pYUFaNh3qrDq4E6RL/bWQXjd75z7WOqYAOi45DMoBJFI9W0A6HVgjhQeTFQBzviJTUHg274
    
    # 头部拼接 iv 后
    ciphertext=ed932439a666f716t9nWfafTcRDHv0KoD/+1t46H7vJ2aYhdXEUAcb+Eqh22whj9w2kO7vHx1pYUFaNh3qrDq4E6RL/bWQXjd75z7WOqYAOi45DMoBJFI9W0A6HVgjhQeTFQBzviJTUHg274
    
  3. 获取当前时间戳 timestamp,并生成随机字串 nonce,按照 ciphertext + nonce + timestamp 的顺序拼接为 ciphertext2(使用 & 拼接字符串):
    nonce=41038640
    
    timestamp=1561458100
    
    ciphertext2=ed932439a666f716t9nWfafTcRDHv0KoD/+1t46H7vJ2aYhdXEUAcb+Eqh22whj9w2kO7vHx1pYUFaNh3qrDq4E6RL/bWQXjd75z7WOqYAOi45DMoBJFI9W0A6HVgjhQeTFQBzviJTUHg274&41038640&1561458100
    
  4. 对第3步的 ciphertext2 使用 HMAC(sha1)获取签名,得到 signature:
    signature=1b9c7db3a0577c62fcac20afcb0400846d374161
    
  5. 发送 Client Id、timestamp、nonce、signature、ciphertext(注意这里没有用到 ciphertext2):
    # url query
    ?client_id=6z2W0hljxBCK2MesrqmFE4pm7Xq0uvVX&timestamp=1561458100&nonce=41038640&signature=1b9c7db3a0577c62fcac20afcb0400846d374161&&method=ENGAGE1-AES-HMAC
    
    # body
    {"ciphertext":"ed932439a666f716t9nWfafTcRDHv0KoD/+1t46H7vJ2aYhdXEUAcb+Eqh22whj9w2kO7vHx1pYUFaNh3qrDq4E6RL/bWQXjd75z7WOqYAOi45DMoBJFI9W0A6HVgjhQeTFQBzviJTUHg274" }