订阅事件后,如果事件被触发,飞连会推送事件数据至已生效的请求地址,您需要在请求地址对应的目标服务器接收并处理事件。
如果事件订阅设置了 Encrypt Key
加密参数,则飞连向请求地址推送的事件数据为加密数据,您使用本地服务器接收事件加密数据后,需要先进行解密操作。
飞连的事件内容采用 AES-256-CBC 加密,加密原理如下:
Encrypt Key
进行哈希,得到密钥 key
。iv
。iv
和 key
对事件内容加密得到 encrypted_event
。base64(iv+encrypted_event)
。本章节分别提供了 Java、Python、Go、Node.js 开发语言的解密示例代码,供您参考。
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); } }
注意
请先执行 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)))
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) }
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 值获取方式:
本地服务器在接收到飞连发送的事件请求后,需要在 3 秒内以 HTTP 200 状态码响应该请求,使飞连系统可以确认本次请求成功被接收,否则飞连系统认为本次推送失败,并会触发重推机制。详情参见重新推送。