本文介绍AIMPaaS回调服务的架构、回调参数、回调列表和签名算法。

简介

AIMPaaS提供了强大的回调能力,让业务方能够在某个调用之前向AppServer发送请求,AppServer可根据回调语义干预后续处理流程。

默认只有AppClient通过IMSDK发起的请求才会有回调,AppServer直接调用AIMPaaS不会发起回调。目前支持的回调请参见回调列表

架构

AIMPaaS的回调服务使用流程如下。

IM回调架构
  1. 业务侧,开发者预先使用控制台登记用于接收回调请求的HTTP URL。
  2. 设置回调URL后,平台会颁发一个用于回调的AppKey和AppSecret。
  3. 平台通过HTTP/HTTPS方式将回调请求发送给AppServer(业务服务端)。
  4. AppServer收到请求后应先校验请求合法性,并尽快进行应答。

注册回调

  1. 业务方预先在控制台上登记用于接收回调请求的HTTP URL,平台会颁发一个用于回调的AppKey和AppSecret。
  2. 平台通过HTTP/HTTPS方式将回调请求发送给AppServer,AppServer收到请求后应先校验请求合法性,并尽快进行应答。

回调参数

请求参数
  • Header
    • method:POST
    • content-type:application/x-www-form-urlencoded
  • Body
    单表解析后如下所示:
    名称 类型 是否必选 示例值 描述
    command String Callback.CreateGroup 回调类型,取值见回调列表。
    data String {"creatorAppUid": "12345", "initMembers": []} 回调请求内容,具体见每个回调的请求参数。
    ispSignature String 876yvrIsoGSox35bolbXPrs7Gvc=" 加签后的值,开发者通过签名校验,HTTP请求的合法性。
    ispSignatureSecretKey String signkeyname 验证签名的Key,开发者需要维护Key对应的密钥。
    requestId String 16A96B9A-F203-4EC5-8E43-CB92E68F4CF8 请求ID。用于全链路追踪。
返回参数
  • Header

    content-type:JSON

  • Body
    单表解析后如下所示:
    名称 类型 是否必选 示例值 描述
    data String "result": { "allow": true, "code": "0xx", "reason": "security " } 回调应答内容,具体见每个回调的应答参数。

回调列表

名称 Command 描述
消息发送回调 Callback.SendMessage 客户端发送的消息,在消息处理之前,会回调业务服务端,业务方可判断消息是否可以发送。
群聊创建回调 Callback.CreateGroup 客户端发起的群聊会话创建,AIMPaaS会回调业务服务端,业务方可校验群是否可创建。
群成员添加回调 Callback.AddGroupMember 客户端发起的群聊会话加人,AIMPaaS会回调业务服务端,业务方可校验加人操作是否允许。
群成员删除回调 Callback.RemoveGroupMember 客户端发起的群聊会话踢人,AIMPaaS会回调业务服务端,业务方校验踢人操作是否允许。
单聊会话创建回调 Callback.CreateSingleChat 客户端发起的单聊会话创建,AIMPaaS会回调业务服务端,会话ID可在回调中生成。AIMPaaS中会话是由会话ID唯一标识。
说明 如果开发者选用了标准单聊,不需要实现该回调。
单聊会话视图创建回调 Callback.AddUserConversation 用户添加单聊会话时,使用该回调业务服务端获取会话视图信息。
说明 如果开发者选用了标准单聊,不需要实现该回调。

回调示例代码

  • 回调请求
    public class CallbackRequest {
        /*
         * 回调命令
         * 开发者根据灰度命令进行分流
         */
        String command;
    
        /*
         * 回调内容
         */
        String data;
    
        /*
         * 加签后的值
         * 开发者通过签名校验,http请求的合法性
         */
        String ispSignature;
    
        /*
         * 验证签名的key
         * 开发者需要维护key对应的密钥
         */
        String ispSignatureSecretKey;
    
        /*
         * 请求ID,用于全链路追踪
         */
        String requestId;
    
        public CallbackRequest() {
        }
    
        public CallbackRequest(String command, String data) {
            this.command = command;
            this.data = data;
        }
    
        public void init(){
        }
    
        public String getCommand() {
            return command;
        }
    
        public void setCommand(String command) {
            this.command = command;
        }
    
        public String getData() {
            return data;
        }
    
        public void setData(String data) {
            this.data = data;
        }
    
        public String getRequestId() {
            return requestId;
        }
    
        public void setRequestId(String requestId) {
            this.requestId = requestId;
        }
    
        public String getIspSignature() {
            return ispSignature;
        }
    
        public void setIspSignature(String ispSignature) {
            this.ispSignature = ispSignature;
        }
    
        public String getIspSignatureSecretKey() {
            return ispSignatureSecretKey;
        }
    
        public void setIspSignatureSecretKey(String ispSignatureSecretKey) {
            this.ispSignatureSecretKey = ispSignatureSecretKey;
        }
    
    }
  • 回调返回
    public class CallbackResponse {
        String data;
    
        public String getData() {
            return data;
        }
    
        public void setData(String data) {
            this.data = data;
        }
    }
  • 回调的URL处理
    import com.alibaba.fastjson.JSON;
    import com.alibaba.fastjson.JSONObject;
    import com.dingtalk.cloud.api.callback.request.*;
    import com.dingtalk.cloud.api.callback.response.*;
    import com.dingtalk.cloud.api.common.AIMCallbackResult;
    import com.dingtalk.impaas.apitest.common.SwitchCenter;
    import com.dingtalk.impaas.apitest.request.CallbackRequest;
    import com.dingtalk.impaas.apitest.request.CallbackResponse;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.tomcat.util.codec.binary.Base64;
    import org.springframework.http.MediaType;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    
    import javax.crypto.Mac;
    import javax.crypto.SecretKey;
    import javax.crypto.spec.SecretKeySpec;
    import javax.xml.crypto.dsig.SignatureMethod;
    import java.io.UnsupportedEncodingException;
    import java.net.URLEncoder;
    
    @Slf4j
    @Controller
    public class MainController2 {
        private static final String CALLBACK_CREATE_GROUPCHAT = "Callback.CreateGroup";
    
        @PostMapping(value = "/callBackTest",
            consumes = {MediaType.APPLICATION_FORM_URLENCODED_VALUE},
            produces = {MediaType.APPLICATION_ATOM_XML_VALUE, MediaType.APPLICATION_JSON_VALUE}
        )
        @ResponseBody String callBackTest(CallbackRequest request) {
            if (!checkSign(request)) {
                // 返回拒绝
                return "";
            }
    
            String rsp = "";
            if (CALLBACK_CREATE_GROUPCHAT.equals(request.getCommand())) {
                rsp = handleGroupChatCreate(request);
            }
            return rsp;
        }
    
        private AIMCallbackResult createAIMCallbackResult(Boolean allow, String code, String reason) {
            AIMCallbackResult result = new AIMCallbackResult();
            result.setAllow(allow);
            result.setCode(code);
            result.setReason(reason);
            return result;
        }
    
        public String handleGroupChatCreate(CallbackRequest param) {
            AIMGroupChatCreateCallbackRequest request = JSON.parseObject(param.getData(), AIMGroupChatCreateCallbackRequest.class);
            AIMGroupChatCreateCallbackResponse resp = new AIMGroupChatCreateCallbackResponse();
    
            if (SwitchCenter.httpServiceAllow) {
                resp.setResult(createAIMCallbackResult(Boolean.TRUE, null, null));
            } else {
                resp.setResult(createAIMCallbackResult(Boolean.FALSE, "code", "reason"));
            }
    
            CallbackResponse callbackResponse = new CallbackResponse();
            callbackResponse.setData(JSONObject.toJSONString(resp));
            return JSONObject.toJSONString(callbackResponse);
        }
    
        public static boolean checkSign(CallbackRequest request) {
            try {
                StringBuilder sb = new StringBuilder();
                sb.append("&").append(percentEncode("command")).append("=").append(percentEncode(request.getCommand()));
                sb.append("&").append(percentEncode("data")).append("=").append(percentEncode(request.getData()));
                sb.append("&").append(percentEncode("ispSignatureSecretKey")).append("=").append(
                    percentEncode(request.getIspSignatureSecretKey()));
                sb.append("&").append(percentEncode("requestId")).append("=").append(percentEncode(request.getRequestId()));
    
                StringBuilder stringToSign = new StringBuilder();
                stringToSign.append(percentEncode("POST"));
                stringToSign.append("&");
                stringToSign.append(percentEncode("/"));
                stringToSign.append("&");
                stringToSign.append(percentEncode(sb.substring(1)));
    
                SecretKey key = new SecretKeySpec(("YourSecretKey" + "&").getBytes("utf-8"), SignatureMethod.HMAC_SHA1);
                Mac hmacSha1 = Mac.getInstance("HmacSHA1");
                hmacSha1.init(key);
    
                String sign = new String(new Base64().encode(hmacSha1.doFinal(stringToSign.toString().getBytes("utf-8"))),
                    "utf-8");
    
                log.info("checkSign in=" + request.getIspSignature() + "    sign" + sign);
                if (sign.equals(request.getIspSignature())) {
                    return true;
                } else {
                    return false;
                }
            } catch (Exception e) {
                log.error("checkSign error. ex=" + e.toString());
                return false;
            }
        }
    
        private static String percentEncode(String s) throws UnsupportedEncodingException {
            if (s == null){
                return null;
            }
            return URLEncoder.encode(s, "utf-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
        }
    }

签名算法


/**
  BodyToSign
    1. 解析HTTP BODY:argsMap = parseForm
    2. 对Form的参数key按照字典序升序排列,sortedArgs
    3. 遍历sortedArgs:"&" + key + "=" + percentEncode("/")
 */
StringToSign = HTTP-Method + "&" + percentEncode("/") + bodyToSign;

Signature = Base64Encode(hmacSha1(AccessSecret + "&" + stringToSign));

/**
    1. 读取Request中除"IspSignature"参数外的所有参数。
    2. 按照参数名称的字典顺序对参数进行排序。
    3. 对每个请求参数的名称和值进行编码。名称和值要使用UTF-8 字符集进行URL编码,URL编码的编码规则是:
        a. 对于字符 A-Z、a-z、0-9 以及字符“-”、“_”、“.”、“~”不编码;
        b. 对于其他字符编码成 “%XY” 的格式,其中 XY 是字符对应 ASCII 码的 16 进制表示。比如英文的双引号 (”)对应的编码就是 %22
    4. 对于扩展的 UTF-8 字符,编码成 “%XY%ZA...” 的格式。
    5. 需要说明的是英文空格( )要被编码是 %20,而不是加号(+)。
    
    注:一般支持 URL 编码的库(比如 Java 中的 java.net.URLEncoder)都是按照 “application/x- www-form-urlencoded” 的 MIME 类型的规则进行编码的。实现时可以直接使用这类方式进行编 码,把编码后的字符串中加号(+)替换成 %20、星号(*)替换成 %2A、%7E 替换回波浪号 (~),即可得到上述规则描述的编码字符串。
    1. 对编码后的参数名称和值使用英文等号(=)进行连接。
    2. 再把英文等号连接得到的字符串按参数名称的字典顺序依次使用&符号连接,即得到规范化请求字符串。
  */
Java版签名实现Demo
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.http.HttpServletRequest;
import javax.xml.crypto.dsig.SignatureMethod;
import org.apache.commons.codec.binary.Base64;


  /**
     * RPC签名
     *
     * @return
     * @throws UnsupportedEncodingException
     */
    private String rpcSign(HttpServletRequest request)
            throws Exception {
        List<String> keys = new ArrayList<String>(request.getParameterMap().keySet());
        Collections.sort(keys);
        StringBuilder sb = new StringBuilder();
        for (String key : keys) {
            if (key.equals("IspSignature")){
                continue;
            }

            String value = request.getParameter(key);
            sb.append("&").append(percentEncode(key)).append("=").append(percentEncode(value));
        }

        StringBuilder stringToSign = new StringBuilder();
        stringToSign.append(percentEncode(request.getMethod()));
        stringToSign.append("&");
        stringToSign.append(percentEncode("/"));
        stringToSign.append("&");
        stringToSign.append(percentEncode(sb.substring(1)));

        SecretKey key = new SecretKeySpec(("YourSecretKey" + "&").getBytes("utf-8"), SignatureMethod.HMAC_SHA1);
        Mac hmacSha1 = Mac.getInstance("HmacSHA1");
        hmacSha1.init(key);

        String sign = new String(new Base64().encode(hmacSha1.doFinal(stringToSign.toString().getBytes("utf-8"))),
                        "utf-8");

        return sign;
    }

 /**
    * 处理特殊字符
    *
    * @param s
    * @return
    * @throws UnsupportedEncodingException
    */
   protected String percentEncode(String s) throws UnsupportedEncodingException {
       if (s == null){
           return null;
       }
       return URLEncoder.encode(s, "utf-8").replace("+", "%20").replace("*", "%2A").replace("%7E", "~");
   }