# 回调事件接入
# 回调配置
- 管理员登录到SCRM平台 -> 企业设置 -> API接入
- 配置系统事件接收URL,Token, EncodingAesKey
# 回调说明
示例:企业在SCRM后台配置系统事件接收URL为http://test.com/callback。
当用户请求触发回调时,会发送回调消息到http://test.com/callback,请求内容如下:
请求方式: POST
请求地址: http://test.com/callback
接收数据格式:
{
"app_key": "co23e51cc5cac543a9",
"token": "123456",
"nonce": "b005326e823f46ecb0ad1902da32316a",
"timestamp": "1623068882",
"encoding_content": "P0+6MsQNLetL0k7aQrzJVANUp2YmZTWbYCMkK7rYCgKv1zwwVa3kWWSiQsHrrq7DUJY+4wi+k/X3LWHODTj+iw==",
"signature": "f12826d7ff6fb212d6f18e6727b18977"
}
参数说明:
字段 | 类型 | 说明 |
---|---|---|
app_key | string | SCRM提供的AppKey |
token | string | SCRM上填写的Token |
nonce | string | 随机字符串 |
timestamp | string | 回调时间戳 |
encoding_content | string | 加密后的回调内容(解密后是一个json结构的字符串) |
signature | string | 签名 |
企业收到消息后,需要作如下处理:
- 对signature进行校验,确保数据安全性
- 解密encoding_content,得到明文的消息结构体
- 正确响应本次请求
- 服务器在五秒内收不到响应会断掉连接
- 当接收成功后,http返回success(不区分大小写)表示接收ok
# 回调示例
以下demo使用伪代码进行说明
假设在SCRM后台有如下配置
AppKey = co23e51cc5cac543a9
Token = 123456
EncodingAESKey = 949001b2d67745328ffa5320feb1950e
收到来自的回调数据
{
"app_key": "co23e51cc5cac543a9",
"token": "123456",
"nonce": "2f5acc3956c3459a8bafc18a97f6db3c",
"timestamp": "1623139834",
"encoding_content": "TDys3S8Q4/jc1YhppJqcX00bCTJZ0vKTiLsKRvYUHBT6+X/Y3M864fidTzucEBtyFlJv3Gw2r/PWPQVjT0vitQ==",
"signature": "7c5775857b111581483998b545502da6"
}
从回调数据获取相关参数
app_key = "co23e51cc5cac543a9" token = "123456" nonce = "2f5acc3956c3459a8bafc18a97f6db3c" timestamp = "1623139834" encoding_content = "TDys3S8Q4/jc1YhppJqcX00bCTJZ0vKTiLsKRvYUHBT6+X/Y3M864fidTzucEBtyFlJv3Gw2r/PWPQVjT0vitQ=="
校验签名
(1) app_key,token,nonce,timestamp,encoding_content这五个参数按照字典序排序
1. "123456" 2. "1623139834" 3. "2f5acc3956c3459a8bafc18a97f6db3c" 4. "TDys3S8Q4/jc1YhppJqcX00bCTJZ0vKTiLsKRvYUHBT6+X/Y3M864fidTzucEBtyFlJv3Gw2r/PWPQVjT0vitQ==" 5. "co23e51cc5cac543a9"
(2)拼接成一个字符串
sort_str = "12345616231398342f5acc3956c3459a8bafc18a97f6db3cTDys3S8Q4/jc1YhppJqcX00bCTJZ0vKTiLsKRvYUHBT6+X/Y3M864fidTzucEBtyFlJv3Gw2r/PWPQVjT0vitQ==co23e51cc5cac543a9"
(3) 对该字符串做MD5加密
md5(sort_str) = "7c5775857b111581483998b545502da6"
(4) 对比接收的signature,发现两者一致,签名校验通过,说明数据没被篡改,是安全的
解密encoding_content数据
(1) 先对encoding_content进行base64解码
msg_encrypt = base64_decode(encoding_content)
(2) 使用EncodingAESKey解密,获取到真实回调数据
data = aes_decrypt(msg_encrypt, EncodingAESKey) // 具体解密流程请参考代码demo
(3) 解密后得到真实数据
{"event_type": 40027, "msg":"这是一段测试数据"}
# 代码示例
签名生成规则,go示例
package utils
import (
"crypto/md5"
)
func GenMd5Signature(data []string) string {
// 1. 字典序排序
sort.Strings(data)
// 2. 拼接成一个字符串
str := ""
for _, v := range data {
str += v
}
// 3. md5加密
h := md5.New()
h.Write([]byte(str))
return hex.EncodeToString(h.Sum(nil))
}
func main() {
// 1. 接收到数据
data := `
{
"app_key": "co23e51cc5cac543a9",
"token": "123456",
"nonce": "2f5acc3956c3459a8bafc18a97f6db3c",
"timestamp": "1623139834",
"encoding_content": "TDys3S8Q4/jc1YhppJqcX00bCTJZ0vKTiLsKRvYUHBT6+X/Y3M864fidTzucEBtyFlJv3Gw2r/PWPQVjT0vitQ==",
"signature": "7c5775857b111581483998b545502da6"
}
`
var dataMap = make(map[string]string)
_ = json.Unmarshal([]byte(data), &dataMap)
// 2. 从回调中获取数据
appKey := dataMap["app_key"]
token := dataMap["token"]
nonce := dataMap["nonce"]
timestamp := dataMap["timestamp"]
encodingContent := dataMap["encoding_content"]
// 生成signature
sign := GenMd5Signature([]string{
appKey,
token,
nonce,
timestamp,
encodingContent,
})
if sign != dataMap["signature"] {
// 数据不安全
}
}
签名生成规则,java示例
package utils
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
public class Md5Tools {
public static String GenMd5Signature(String[] data) throws NoSuchAlgorithmException {
// 1. 字典序排序
Arrays.sort(data);
// 2. 拼接成一个字符串
StringBuilder encryStr = new StringBuilder();
for (String v : data) {
encryStr.append(v);
}
// 3. md5加密
MessageDigest m = MessageDigest.getInstance("MD5");
m.update(encryStr.toString().getBytes());
byte[] s = m.digest();
StringBuilder result = new StringBuilder();
for (byte b : s) {
result.append(Integer.toHexString((0x000000FF & b) | 0xFFFFFF00).substring(6));
}
return result.toString();
}
public static void main(String[] args) throws NoSuchAlgorithmException {
String[] dataArr = new String[5];
dataArr[0] = "co23e51cc5cac543a9";
dataArr[1] = "123456";
dataArr[2] = "1623139834";
dataArr[3] = "2f5acc3956c3459a8bafc18a97f6db3c";
dataArr[4] = "TDys3S8Q4/jc1YhppJqcX00bCTJZ0vKTiLsKRvYUHBT6+X/Y3M864fidTzucEBtyFlJv3Gw2r/PWPQVjT0vitQ==";
String a = GenMd5Signature(dataArr);
System.out.println(a);
}
}
签名生成规则,C#示例
ModelCallBackReq callBackReq = new ModelCallBackReq
{
app_key = "co23e51cc5cac543a9",
token = "123456",
nonce = "2f5acc3956c3459a8bafc18a97f6db3c",
timestamp = "1623139834",
signature = "7c5775857b111581483998b545502da6",
encoding_content = "TDys3S8Q4/jc1YhppJqcX00bCTJZ0vKTiLsKRvYUHBT6+X/Y3M864fidTzucEBtyFlJv3Gw2r/PWPQVjT0vitQ==",
};
string[] arrayContent = new string[] { callBackReq.app_key, callBackReq.token, callBackReq.nonce, callBackReq.timestamp, callBackReq.encoding_content };
Array.Sort(arrayContent);
string sortStr = string.Join("", arrayContent);
//签名
string signa = Utils.md5(sortStr);
encoding_content解密算法,go示例
package utils
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
)
// 解密主流程函数
func Decrypt(encodingContent, encodingAesKey string) (string, error) {
// 1. base64解码
base64DeContent, err := base64.StdEncoding.DecodeString(encodingContent)
if err != nil {
return "", err
}
// 2. aes解密
deContent := AesDecryptCBC(base64DeContent, []byte(encodingAesKey))
return string(deContent), nil
}
// Aes解密
func AesDecryptCBC(encrypted []byte, key []byte) (decrypted []byte) {
block, _ := aes.NewCipher(key) // 分组秘钥
blockSize := block.BlockSize() // 获取秘钥块的长度
blockMode := cipher.NewCBCDecrypter(block, key[:blockSize]) // 加密模式
decrypted = make([]byte, len(encrypted)) // 创建数组
blockMode.CryptBlocks(decrypted, encrypted) // 解密
decrypted = pkcs5UnPadding(decrypted) // 去除补全码
return decrypted
}
func pkcs5UnPadding(originData []byte) []byte {
length := len(originData)
if length == 0 {
return nil
}
unpadding := int(originData[length-1])
return originData[:(length - unpadding)]
}
func main() {
aesKey := "949001b2d67745328ffa5320feb1950e"
content := "P0+6MsQNLetL0k7aQrzJVANUp2YmZTWbYCMkK7rYCgKv1zwwVa3kWWSiQsHrrq7DUJY+4wi+k/X3LWHODTj+iw=="
decryptedContent, err := Decrypt(content, aesKey)
if err != nil {
// 错误处理
}
// 解密后的数据
// {"event_type": 40027, "msg":"这是一段测试数据"}
println(decryptedContent)
}
encoding_content解密算法,java示例
public class AesTools {
public static byte[] str2ByteArr(String hexStr) {
return hexStr.getBytes();
}
public static String DecryptCBC(String sSrc, String encodingAESKey) {
try {
int base = 16;
byte[] raw = str2ByteArr(encodingAESKey);
byte[] ivRaw = new byte[base];
System.arraycopy(raw, 0, ivRaw, 0, base);
SecretKeySpec skeySpec = new SecretKeySpec(raw, "AES");
Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding");
IvParameterSpec iv = new IvParameterSpec(ivRaw);
cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);
// 1. 先用base64解密
byte[] encrypted = Base64.getDecoder().decode(sSrc);
// 2. AES解密
try {
byte[] original = cipher.doFinal(encrypted);
return new String(original);
} catch (Exception e) {
System.out.println(e.toString());
return null;
}
} catch (Exception ex) {
System.out.println(ex.toString());
return null;
}
}
public static void main(String[] args) throws Exception {
// 后台配置的aes key
String encodingAESKey = "588bc7cfb5a34507ba132cc75b6df005";
String content = "Kuf/0j1nvhFMxVGadSBd6E9xgZqvEvAOA1BuO514GUJK/GjnfzccnUP0c0VZ/T5E6E+1ASiLqc8KTEYqoUsKqVBLUtCC1VW/qKY46snL/uGCI3VWP86Uk74q3EoMzzBHSWZXqxOACoNi+ETnktWEwbe6lP34nQweuULqrw5d9ft/mlyOEYeRODObLFJhqLdXAQll/335sXrMKCtIQV/Pw+PCncG6/gtEMscdH3T6PnBHrhoM0dPXZtdrCE819h4DaQ+OLnCXHTTpj8ljX/Ap/rOLOfBMOvJjt3Bm1bq14i+3QgpP5nPJxv50YrdE9mX3ksP1UgjbUjZxffQCNsJYob7/W83O6Mo36MwzCse6er8kqHwaBUQlcSc8S7Jly68Te+vZKJ+7C0j57T6/3VYnWoP5VJVQEfHvdaqbTL72QG89W8XuPVz5DttvUv34htX/bFEW813NOoWsrg1piXyXeFNZ9oFshShgcDN7Gymjqz0eC0cH4re934sk3m5LgFrb";
String a = DecryptCBC(content, encodingAESKey);
System.out.println(a);
}
}
encoding_content解密算法,php示例
<?php
$key = "949001b2d67745328ffa5320feb1950e";
$content = "P0+6MsQNLetL0k7aQrzJVANUp2YmZTWbYCMkK7rYCgKv1zwwVa3kWWSiQsHrrq7DUJY+4wi+k/X3LWHODTj+iw==";
function stripPKSC5Padding($source) {
$num = ord(substr($source, - 1));
if ($num == 125) {
return $source;
}
return substr($source, 0, -$num);
}
// 加密过程
function encrypt($origData, $key)
{
$cipher = "AES-256-CBC";
$length = openssl_cipher_iv_length($cipher);
$iv = substr($key, 0, $length);
$ciphertext_raw = openssl_encrypt($origData, $cipher, $key, OPENSSL_RAW_DATA, $iv);
return base64_encode($ciphertext_raw);
}
// 解密过程
function decrypt($data, $key)
{
$cipher = "AES-256-CBC";
$c = base64_decode($data);
$length = openssl_cipher_iv_length($cipher);
$iv = substr($key, 0, $length);
$decodeData = openssl_decrypt($data, $cipher, $key, OPENSSL_ZERO_PADDING, $iv);
// 替换换行符等
$ret = str_replace(["\n"],"",$decodeData);
return stripPKSC5Padding($ret);
}
print_r(decrypt($content, $key));
encoding_content解密算法,C#示例
public static string Decrypt(string Data, string Key)
{
Byte[] encryptedBytes = Convert.FromBase64String(Data);
Byte[] bKey = new Byte[32];
Array.Copy(Encoding.UTF8.GetBytes(Key.PadRight(bKey.Length)), bKey, bKey.Length);
MemoryStream mStream = new MemoryStream(encryptedBytes);
RijndaelManaged aes = new RijndaelManaged();
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.KeySize = 128;
byte[] iv = bKey.Skip(0).Take(16).ToArray();
aes.IV = iv;
aes.Key = bKey;
CryptoStream cryptoStream = new CryptoStream(mStream, aes.CreateDecryptor(), CryptoStreamMode.Read);
try
{
byte[] tmp = new byte[encryptedBytes.Length + 32];
int len = cryptoStream.Read(tmp, 0, encryptedBytes.Length + 32);
byte[] ret = new byte[len];
Array.Copy(tmp, 0, ret, 0, len);
return Encoding.UTF8.GetString(ret);
}
finally
{
cryptoStream.Close();
mStream.Close();
aes.Clear();
}
}
static void Main(string[] args)
{
aesKey := "949001b2d67745328ffa5320feb1950e"
content := "P0+6MsQNLetL0k7aQrzJVANUp2YmZTWbYCMkK7rYCgKv1zwwVa3kWWSiQsHrrq7DUJY+4wi+k/X3LWHODTj+iw=="
//回调内容解密
string decrypContent = Utils.Decrypt(content, aesKey);
Console.WriteLine(decrypContent);
}
← 开放接口接入 第三方应用回调事件接入 →