You need to enable JavaScript to run this app.
导航
接收事件
最近更新时间:2025.03.28 09:56:53首次发布时间:2025.03.28 09:56:53
我的收藏
有用
有用
无用
无用

订阅事件后,如果事件被触发,飞连会推送事件数据至已生效的请求地址,您需要在请求地址对应的目标服务器接收并处理事件。

解密事件

如果事件订阅设置了 Encrypt Key 加密参数,则飞连向请求地址推送的事件数据为加密数据,您使用本地服务器接收事件加密数据后,需要先进行解密操作。

加密原理

飞连的事件内容采用 AES-256-CBC 加密,加密原理如下:

  1. 使用 SHA256 对 Encrypt Key 进行哈希,得到密钥 key
  2. 使用 PKCS7Padding 方式将事件内容进行填充。
  3. 生成 16 字节的随机数作为初始向量 iv
  4. 使用 ivkey 对事件内容加密得到 encrypted_event
  5. 目标服务器收到的密文为 base64(iv+encrypted_event)

解密方法

本章节分别提供了 Java、Python、Go、Node.js 开发语言的解密示例代码,供您参考。

Java

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class Decrypt {
    public static void main(String[] args) throws Exception {
        Decrypt d = new Decrypt("testkey");

        // {"challenge":"f8f76934-fcf2-49e4-8593-4cfa92401234","token":"self-test","type":"url_verification"}
       System.out.println(d.decrypt("byxnrhs61Lk8dOCd7WEOoDA6ypOpFsM4zYahSGTKOYv2CFrRyAuHm2CB162eI2vFvn/RX3IJpwMgMTwADgjiLuEXzsvW70skf1RD5Ex2dyMOhbE70Np7m7u6ks/YxF1fSkHewOCW82IV5K+SBCqkMKntWOmSsU4123123=")); 
    }

    private byte[] keyBs;

    public Decrypt(String key) {
        MessageDigest digest = null;
        try {
            digest = MessageDigest.getInstance("SHA-256");
        } catch (NoSuchAlgorithmException e) {
            // won't happen
        }
        keyBs = digest.digest(key.getBytes(StandardCharsets.UTF_8));
    }

    public String decrypt(String base64) throws Exception {
        byte[] decode = Base64.getDecoder().decode(base64);
        Cipher cipher = Cipher.getInstance("AES/CBC/NOPADDING");
        byte[] iv = new byte[16];
        System.arraycopy(decode, 0, iv, 0, 16);
        byte[] data = new byte[decode.length - 16];
        System.arraycopy(decode, 16, data, 0, data.length);
        cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(keyBs, "AES"), new IvParameterSpec(iv));
        byte[] r = cipher.doFinal(data);
        if (r.length > 0) {
            int p = r.length - 1;
            for (; p >= 0 && r[p] <= 16; p--) {
            }
            if (p != r.length - 1) {
                byte[] rr = new byte[p + 1];
                System.arraycopy(r, 0, rr, 0, p + 1);
                r = rr;
            }
        }
        return new String(r, StandardCharsets.UTF_8);
    }
}

Python 3

注意

请先执行 pip install pycryptodome 以支持引入 AES 方法。

import hashlib
import base64
from Crypto.Cipher import AES

class  AESCipher(object):
    def __init__(self, key):
        self.bs = AES.block_size
        self.key=hashlib.sha256(AESCipher.str_to_bytes(key)).digest()
    @staticmethod
    def str_to_bytes(data):
        u_type = type(b"".decode('utf8'))
        if isinstance(data, u_type):
            return data.encode('utf8')
        return data
    @staticmethod
    def _unpad(s):
        return s[:-ord(s[len(s) - 1:])]
    def decrypt(self, enc):
        iv = enc[:AES.block_size]
        cipher = AES.new(self.key, AES.MODE_CBC, iv)
        return  self._unpad(cipher.decrypt(enc[AES.block_size:]))
    def decrypt_string(self, enc):
        enc = base64.b64decode(enc)
        return  self.decrypt(enc).decode('utf8')


if __name__ =="__main__":
    encrypt = "byxnrhs61Lk8dOCd7WEOoDA6ypOpFsM4zYahSGTKOYv2CFrRyAuHm2CB162eI2vFvn/RX3IJpwMgMTwADgjiLuEXzsvW70skf1RD5Ex2dyMOhbE70Np7m7u6ks/YxF1fSkHewOCW82IV5K+SBCqkMKntWOmSs123123="
    cipher = AESCipher("testkey")
    # {"challenge":"f8f76934-fcf2-49e4-8593-4cfa92401234","token":"self-test","type":"url_verification"}
    print("明文:\n{}".format(cipher.decrypt_string(encrypt)))

Go

package main

import (
    "crypto/aes"
    "crypto/cipher"
    "crypto/sha256"
    "encoding/base64"
    "errors"
    "fmt"
)

// Decrypt 数据解密
func Decrypt(encrypt string, key string) (string, error) {
    buf, err := base64.StdEncoding.DecodeString(encrypt)
    if err != nil {
       return "", fmt.Errorf("base64StdEncode Error[%v]", err)
    }
    if len(buf) < aes.BlockSize {
       return "", errors.New("cipher  too short")
    }
    keyBs := sha256.Sum256([]byte(key))
    block, err := aes.NewCipher(keyBs[:sha256.Size])
    if err != nil {
       return "", fmt.Errorf("AESNewCipher Error[%v]", err)
    }
    iv := buf[:aes.BlockSize]
    buf = buf[aes.BlockSize:]
    // CBC mode always works in whole blocks.
    if len(buf)%aes.BlockSize != 0 {
       return "", errors.New("ciphertext is not a multiple of the block size")
    }
    mode := cipher.NewCBCDecrypter(block, iv)
    mode.CryptBlocks(buf, buf)

    unPadding := int(buf[len(buf)-1])
    if len(buf)-unPadding < 0 {
       return "", errors.New("pcks7 unpadding failed")
    }
    return string(buf[:(len(buf) - unPadding)]), nil
}


func main() {
    encryptMsg := "byxnrhs61Lk8dOCd7WEOoDA6ypOpFsM4zYahSGTKOYv2CFrRyAuHm2CB162eI2vFvn/RX3IJpwMgMTwADgjiLuEXzsvW70skf1RD5Ex2dyMOhbE70Np7m7u6ks/YxF1fSkHewOCW82IV5K+SBCqkMKnt123123="
    result, err := Decrypt(encryptMsg, "testkey")
    if err != nil {
       panic(err)
    }

    // {"challenge":"f8f76934-fcf2-49e4-8593-4cfa92401234","token":"self-test","type":"url_verification"}
    fmt.Println(result)
}

Node.js

const crypto = require("crypto");
class AESCipher {
    constructor(key) {
        const hash = crypto.createHash('sha256');
        hash.update(key);
        this.key = hash.digest();
    }
    decrypt(encrypt) {
        const encryptBuffer = Buffer.from(encrypt, 'base64');
        const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, encryptBuffer.slice(0, 16));
        let decrypted = decipher.update(encryptBuffer.slice(16).toString('hex'), 'hex', 'utf8');
        decrypted += decipher.final('utf8');
        return decrypted;
    }
}
encrypt = "byxnrhs61Lk8dOCd7WEOoDA6ypOpFsM4zYahSGTKOYv2CFrRyAuHm2CB162eI2vFvn/RX3IJpwMgMTwADgjiLuEXzsvW70skf1RD5Ex2dyMOhbE70Np7m7u6ks/YxF1fSkHewOCW82IV5K+SBCqkMKntWOm123123="
cipher = new AESCipher("testkey")

// {"challenge":"f8f76934-fcf2-49e4-8593-4cfa92401234","token":"self-test","type":"url_verification"}
console.log(cipher.decrypt(encrypt))

处理事件

直接获取事件数据或经过解密获取事件数据后,需要处理事件,以确保请求来自飞连,且成功接收到了事件。

安全校验

根据事件订阅加密策略的 Verification Token 参数进行安全校验,以确保接收到的请求来自飞连而非伪造。只需要从接收到的事件结构体中,获取 header 参数中的 token 值,将该值与飞连内的 Verification Token 值进行比较。如果相同则表示请求来自飞连,不相同则表示该请求为伪造的风险请求。
飞连内事件订阅的 Verification Token 值获取方式:

  1. 登录飞连管理后台。
  2. 在左侧导航栏,选择集成管理 > 事件订阅
  3. 在指定事件订阅右侧单击编辑
  4. 在事件详情页底部,获取 Verification Token

响应请求

本地服务器在接收到飞连发送的事件请求后,需要在 3 秒内以 HTTP 200 状态码响应该请求,使飞连系统可以确认本次请求成功被接收,否则飞连系统认为本次推送失败,并会触发重推机制。详情参见重新推送