接口回调


填写回调地址

填写回调地址,例如:https://example.com/callback。 为了安全,强烈推荐使用https。

验证回调参数以及解密数据

WeLink每一次访问回调URL的时候将发出如下请求(下面链接中的值只是示例,并不代表真实值):

POST https://您服务端部署的IP:您的端口/callback
# 包含的json数据如下,其中encrypt是经过加密的消息体(采用base64编码), 其中包含事件类型以及相应的一些数据
{"encrypt": "MDgwNTY0NDIxODUyMjA3OA==RTE3Q0E5MEI5RDIyRjlFRkJGNDg3NTYyQTA3ODI0NEE2OTU4NjE0NDJBRDg0OURDQTVDNEFBNTlEODM0NkQ1MjJFMkM1ODVENzlDMENGQTFDOThBRkRGNzc2QjU1MjYwNUMyNDk1NkE5Mjc0RjUxOTdFMDM5MkQzRDIzRTg4M0M1NUY5ODQxNTZFOTIwNEVBQ0EyMkJEMzkxNUExQkM2Q0NCN0EyODdE"}

encrypt内的数据为加密后的消息体,采用AES GCM算法,采用16字节的二进制偏移量(base64编码后就是24字节), 解密后的数据结构如下:

{
  "eventType": "xxx", 
  "timestamp": "1565167553",
  "xxx": "xxx"
}

为防止重放攻击,请务必判断timestamp(Unix的秒级时间戳)是否与您服务器的时间偏差在允许的时间范围内, 这里推荐30min内的时间是合理的。具体的解密见附录。

返回结果

返回结果也需要进行带上timestamp加密,防止重放攻击以及验证回调是否被正确推送到应用。
应用返回原始数据结构应如下:

{
    "msg": "success",
    "timestamp": "1565167553"
}

经过AES GCM算法加密后(前16字节为偏移量),先进行base64编码(24字节)再和base64编码的密文拼接,拼成json格式。以上数据经过加密后,返回的结果应该为:

{"encrypt": "NjA0NTQ4MzM0MTExMjQ3NQ==MzhEMTY5RDI2Qjg4RjRDRTEwNUZBRTMyNjcxNTlCNDcyODUyNzEzQkUzOEU1Qzc3ODc2MjlFRkUzMzlGM0JCMTQ5QURBM0VCODA1QjExRTQ5NkI5Mjc0MzRCMTI3OTExNEI3RjU1RDRDNDNGNEE2MA=="}

需要注意的是,WeLink设置timestamp偏差超过30min时会认为返回值非法,推荐开发者直接以WeLink请求参数中timestamp返回即可。

回调类型

事件类型 eventType 请求示例 原文
测试 test {"encrypt": "xxx"} {"eventType": "test","timestamp": "1562752619"}
企业授权事件 corpAuth {"encrypt": "xxx"} {"eventType": "corpAuth","tenantId": "E22BD2CC5F8B4EE69B931113F678A694","timestamp": "1562752619"}
企业解除授权事件 corpCancelAuth {"encrypt": "xxx"} {"eventType": "corpCancelAuth","tenantId": "E22BD2CC5F8B4EE69B931113F678A694","timestamp": "1562752619"}

附录

import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * 加密和解密的示例代码
 */
public class Main{

    private static final String ALGORITHM = "AES";
    private static final String defaultCharset = "UTF-8";
    private static final String KEY_GCM_AES = "AES/GCM/NoPadding";
    private static final int AES_KEY_SIZE = 128;

    /**
     * 生成length字节的偏移量IV
     * createIV的功能<br>
     *
     * @param length
     * @return
     * @throws NoSuchAlgorithmException
     */
    public static String createIV(int length) throws NoSuchAlgorithmException {
        SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
        byte[] salt = new byte[length];
        random.nextBytes(salt);
        return encodeToBase64(salt);
    }

    /**
     * 解密——使用自定义的加密key
     *
     * @param data
     * @param key
     * @param ivStr
     * @return
     * @throws Exception
     */
    public static String decryptByGcm(String data, String key, String ivStr) throws Exception {
        Cipher cipher = Cipher.getInstance(KEY_GCM_AES);

        byte[] iv = decodeFromBase64(ivStr);
        SecretKeySpec keySpec = getSecretKeySpec(key);
        cipher.init(2, keySpec, new GCMParameterSpec(AES_KEY_SIZE, iv));
        byte[] content = decodeFromBase64(data);
        byte[] result = cipher.doFinal(content);
        return new String(result);
    }

    /**
     * 加密——使用自定义的加密key
     *
     * @param data
     * @param key
     * @param ivStr
     * @return
     * @throws Exception
     */
    public static String encryptByGcm(String data, String key, String ivStr) throws Exception {
        Cipher cipher = Cipher.getInstance(KEY_GCM_AES);

        byte[] iv = decodeFromBase64(ivStr);
        SecretKeySpec keySpec = getSecretKeySpec(key);
        cipher.init(1, keySpec, new GCMParameterSpec(AES_KEY_SIZE, iv));
        byte[] content = data.getBytes(defaultCharset);
        byte[] result = cipher.doFinal(content);
        return encodeToBase64(result);
    }

    private static byte[] decodeFromBase64(String data) {
        return Base64.getDecoder().decode(data);
    }

    private static String encodeToBase64(byte[] data) {
        return Base64.getEncoder().encodeToString(data);
    }

    /**
     * 公共使用,获取SecretKeySpec
     *
     * @param key
     * @return
     */
    private static SecretKeySpec getSecretKeySpec(String key) {
        SecretKeySpec keySpec = null;
        try {
            //1.构造密钥生成器,指定为AES算法,不区分大小写
            KeyGenerator kgen = KeyGenerator.getInstance(ALGORITHM);
            //2.根据ecnodeRules规则初始化密钥生成器
            //生成一个128位的随机源,根据传入的字节数组
            SecureRandom secureRandom= SecureRandom.getInstance("SHA1PRNG");
            secureRandom.setSeed(key.getBytes(defaultCharset));
            kgen.init(AES_KEY_SIZE, secureRandom);
            //3.产生原始对称密钥
            SecretKey secretKey = kgen.generateKey();
            //4.获得原始对称密钥的字节数组
            byte[] enCodeFormat = secretKey.getEncoded();
            //5.根据字节数组生成AES密钥
            keySpec = new SecretKeySpec(enCodeFormat, ALGORITHM);
        } catch (Exception e) {
            System.out.println("To do get SecretKeySpec exception!");
        }
        return keySpec;
    }

    public static void main(String[] args) throws Exception {
        String secret = "8cf860c0-30b7-4357-a104-fa627c59085d";//app的secret,加解密使用,开放平台获得
        String reqJson="{\"eventType\":\"corpAuth\",\"tenantId\":\"tenant\",\"timestamp\":\"1565167553\"}";
        String respJson="{\"timestamp\":\"1565167553\",\"msg\":\"success\"}";
        System.out.println("[请求Json]:"+reqJson);
        String ivStr = createIV(16);
        String reqStr=encryptByGcm(reqJson,secret, ivStr);
        System.out.println("[请求密文]:" + ivStr + reqStr);
        String reqJsonAfterDecrypt=decryptByGcm(reqStr, secret, ivStr);
        System.out.println("[请求密文解析后]:"+reqJsonAfterDecrypt);
        String ivStr2 = createIV(16);
        System.out.println("[响应Json]:"+respJson);
        String respStr=encryptByGcm(respJson,secret, ivStr2);
        System.out.println("[响应密文]:" + ivStr2 + respStr);
        String respJsonAfterDecrypt=decryptByGcm(respStr,secret, ivStr2);
        System.out.println("[响应密文解析后]:"+respJsonAfterDecrypt);
    }
}

/*
[请求Json]:{"eventType":"corpAuth","tenantId":"tenant","timestamp":"1565167553"}
[请求密文]:PGkTPQrrTwlqBEu5pzPyxw==3BWfWmYTj67h5qdD4og6el7GrxaXHqm0gndcv/X8zK6j9ablMO+571LbjQWJJogcIunLPkJf9Yo4iHAP+QIB3KcihrLj3IHrRhbE8KuQvzCPVAo=
[请求密文解析后]:{"eventType":"corpAuth","tenantId":"tenant","timestamp":"1565167553"}
[响应Json]:{"timestamp":"1565167553","msg":"success"}
[响应密文]:5wwd5oVCbwgvaGzE2W9vPg==kdG1FYbicMlNY77ALZdBtC1ylS0aF+jzff8iyq2Ro1SJqUQCTAG96hLp+A7OyX/Im8IoFQ1XtfE=
[响应密文解析后]:{"timestamp":"1565167553","msg":"success"}
*/

result. ""

    Not Found. ""