card.action.trigger
回调,即卡片回传交互回调。功能 | 回调结构 | 相关文档 |
---|---|---|
链接预览 | 实现链接预览功能必须订阅 拉取链接预览数据 回调,该回调对应的回调参数、响应参数说明,可参见拉取链接预览数据。 | 了解链接预览功能,以及如何配置链接预览,参见链接预览开发指南。 |
飞书卡片 | 如果你构建的飞书卡片可通过交互组件完成业务处理,则需要订阅 卡片回传交互 回调,该回调对应的回调参数、响应参数说明,可参见卡片回传交互。 | 了解飞书卡片功能,参见飞书卡片概述。 了解飞书卡片请求回调实现流程,参见配置卡片交互。 |
操作 | 是否必须 | 描述 |
---|---|---|
安全校验 | 否 | 安全校验用于确认业务服务器接收到的请求来自飞书开放平台,而不是伪造的风险请求。 |
回调解密 | 否 | 建议为应用配置 Encrypt Key,配置后推送的回调请求为加密数据,能够确保请求数据安全性。相应的,业务服务器收到请求后,需要进行解密,才可以获取到真实的回调数据。 |
响应回调请求 | 是 | 业务服务器在收到回调请求后,必须返回响应结果,否则会被系统判断为回调失败。 |
校验方式 | 使用说明 |
---|---|
签名校验 | 如果你在飞书应用内配置了 Encrypt Key 加密策略,需使用签名校验,这种校验方式相对复杂,但是安全性高,且无需解密和解析回调即可完成安全校验。校验方式如下: 1. 获取 encrypt_key 。在应用管理平台的 事件与回调 > 加密策略 页面,可以查看 encrypt_key 。![]() 2. 校验请求来源,示例代码可参考下文签名校验示例代码。 将请求头 X-Lark-Request-Timestamp 、X-Lark-Request-Nonce 与 encrypt_key 拼接后,按照 encode('utf-8') 编码得到 byte[] b1 ,再拼接上请求的原始 body,得到一个 byte[] b 。将 b 用 sha256 算法得到字符串 s , 校验 s 是否和请求头 X-Lark-Signature 一致。该方式无需解密回调可完成安全校验,但获取回调内容仍需解密,回调解密操作参考下文回调解密。 |
Verification Token 校验 | 飞书应用默认配置了 Verification Token,你可以在业务服务器内接收回调请求,并在请求体中获取 Verification Token 值,将该值与飞书应用内的 Verification Token 值进行比对,取值相同则说明该请求来自飞书开放平台的指定应用。 这种校验方式简单,但是安全性较低,在未配置 Encrypt Key 加密策略的前提下,会明文传输 Verification Token,存在数据泄露风险。 Verification Token 可以在应用管理平台的 事件与回调 > 加密策略 页面获取,并与回调中解析出的 Verification Token 进行对比。 ![]() |
参数 | 说明 |
---|---|
timestamp | 对应请求头中的 X-Lark-Request-Timestamp |
nonce | 对应请求头中的 X-Lark-Request-Nonce |
encrypt_key | 从开发者后台获取到的应用 Encrypt Key |
import hashlib
bytes_b1 = (timestamp + nonce + encrypt_key).encode('utf-8')
bytes_b = bytes_b1 + body
h = hashlib.sha256(bytes_b)
signature = h.hexdigest()
# check if request headers['X-Lark-Signature'] equals to signature
import org.apache.commons.codec.binary.Hex;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
class Main {
public String calculateSignature(String timestamp, String nonce, String encryptKey, String bodyString) throws NoSuchAlgorithmException {
StringBuilder content = new StringBuilder();
content.append(timestamp).append(nonce).append(encryptKey).append(bodyString);
MessageDigest alg = MessageDigest.getInstance("SHA-256");
String sign = Hex.encodeHexString(alg.digest(content.toString().getBytes()));
return sign;
}
}
import (
"crypto/sha256"
"fmt"
)
func calculateSignature(timestamp, nonce, encryptKey, bodystring string) string {
var b strings.Builder
b.WriteString(timestamp)
b.WriteString(nonce)
b.WriteString(encryptKey)
b.WriteString(bodystring) //bodystring 指整个请求体,不要在反序列化后再计算
bs := []byte(b.String())
h := sha256.New()
h.Write(bs)
bs = h.Sum(nil)
sig := fmt.Sprintf("%x", bs)
return sig
}
var crypto = require('crypto');
function calculateSignature(timestamp, nonce, encryptKey, body) {
const content = timestamp + nonce + encryptKey + body
const sign = crypto.createHash('sha256').update(content).digest('hex');
return sign
}
using System.Security.Cryptography;
public static string calculateSignature(string timestamp, string nonce, string encryptKey, string body) {
StringBuilder content = new StringBuilder();
content.Append(timestamp);
content.Append(nonce);
content.Append(encryptKey);
content.Append(body);
SHA256 sha256 = new SHA256CryptoServiceProvider();
byte[] bytes_out = sha256.ComputeHash(Encoding.UTF8.GetBytes(content.ToString()));
string result = BitConverter.ToString(bytes_out);
result = result.Replace("-", "");
return result;
}
<?php
$encrypt_key = ""; // 开放平台后台的 Encrypt Key
$timestamp = "";
$nonce = "";
$body = ""; // 指整个请求体,不要在反序列化后再计算
$signature = hash("sha256", $timestamp . $nonce . $encrypt_key . $body);
// check if request headers['X-Lark-Signature'] equals to signature
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')
encrypt = "P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk="
cipher = AESCipher("test key")
print("明文:\n{}".format(cipher.decrypt_string(encrypt)))
package com.larksuite.oapi.sample;
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("test key");
System.out.println(d.decrypt("P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=")); //hello world
}
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);
}
}
package main
import (
"crypto/aes"
"crypto/cipher"
"crypto/sha256"
"encoding/base64"
"errors"
"fmt"
"strings"
)
func main() {
s, err := Decrypt("P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=", "test key")
if err != nil {
panic(err)
}
fmt.Println(s) //hello world
}
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)
n := strings.Index(string(buf), "{")
if n == -1 {
n = 0
}
m := strings.LastIndex(string(buf), "}")
if m == -1 {
m = len(buf) - 1
}
return string(buf[n : m+1]), nil
}
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Text;
namespace decrypt
{
class AESCipher
{
const int BlockSize = 16;
private byte[] key;
public AESCipher(string key)
{
this.key = SHA256Hash(key);
}
public string DecryptString(string enc)
{
byte[] encBytes = Convert.FromBase64String(enc);
RijndaelManaged rijndaelManaged = new RijndaelManaged();
rijndaelManaged.Key = this.key;
rijndaelManaged.Mode = CipherMode.CBC;
rijndaelManaged.IV = encBytes.Take(BlockSize).ToArray();
ICryptoTransform transform = rijndaelManaged.CreateDecryptor();
byte[] blockBytes = transform.TransformFinalBlock(encBytes, BlockSize, encBytes.Length - BlockSize);
return System.Text.Encoding.UTF8.GetString(blockBytes);
}
public static byte[] SHA256Hash(string str)
{
byte[] bytes = Encoding.UTF8.GetBytes(str);
SHA256 shaManaged = new SHA256Managed();
return shaManaged.ComputeHash(bytes);
}
public static void Main(string[] args)
{
string encrypt = "P37w+VZImNgPEO1RBhJ6RtKl7n6zymIbEG1pReEzghk=";
AESCipher cipher = new AESCipher("test key");
Console.WriteLine(cipher.DecryptString(encrypt));
}
}
}
<?php
$encrypt_data = ""; // 待解密的信息
$encrypt_key = ""; // 从开发者后台获取 Encrypt Key
$base64_decode_message = base64_decode($encrypt_data);
$iv = substr($base64_decode_message, 0, 16);
$encrypted_event = substr($base64_decode_message, 16);
$decrypt = openssl_decrypt($encrypted_event, 'AES-256-CBC', hash('sha256', $encrypt_key, true), OPENSSL_RAW_DATA, $iv);
print($decrypt);
// get the real event
功能 | 回调结构 | 相关文档 |
---|---|---|
链接预览 | 实现链接预览功能必须订阅 拉取链接预览数据 回调,该回调对应的回调参数、响应参数说明,可参见拉取链接预览数据。 | 了解链接预览功能,以及如何配置链接预览,参见链接预览开发指南。 |
飞书卡片 | 如果你构建的飞书卡片可通过交互组件完成业务处理,则需要订阅 卡片回传交互 回调,该回调对应的回调参数、响应参数说明,可参见卡片回传交互。 | 了解飞书卡片功能,参见飞书卡片概述。了解飞书卡片请求回调实现流程,参见配置卡片交互。 |