接口回调
填写回调地址
填写回调地址,例如: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"}
*/