飞书开放平台的API并不是一套复杂的RPC协议,所有接口都基于HTTPS的RESTful风格,使用JSON作为数据载体。对于C#开发者来说,对接飞书API本质上就是构造正确的请求头、请求体,再用HttpClient发送出去。但真正麻烦的地方在于:鉴权token需要提前获取并定期刷新,不同接口要求的权限范围也不一样,事件回调还要处理验签和响应格式。本文会从零开始,演示如何用C#完成一个可复用的飞书API对接类库,覆盖获取tenant_access_token、发送文本消息、发送交互式卡片消息以及回调验签四个关键环节。

一、创建飞书自建应用并配置权限
飞书开放平台提供了两种常见的应用类型:企业自建应用和商店应用。对于内部系统集成来说,自建应用是最直接的选择。登录飞书开放平台后台,创建一个企业自建应用,创建完成后会得到两个核心凭证:App ID和App Secret。App ID是应用的唯一标识,App Secret则用于请求token时的签名,绝对不能泄露到客户端代码中。
创建应用之后,需要进入权限管理页面,为应用添加必要的API权限。本文示例会用到三个权限:获取tenant_access_token、发送消息到群聊、接收事件回调。其中发送消息涉及im:message和im:message:send_as_bot权限,接收事件则需要开通事件订阅并配置请求地址。如果是通过机器人向指定用户或群聊发消息,还需要在应用功能中启用机器人能力,并把机器人添加到对应群聊中。
权限配置完成后,建议先在飞书开放平台提供的API调试台里手动调用一次获取token的接口,确认App ID和App Secret无误。调试台返回的tenant_access_token有效期通常为2小时,过期后需要重新获取。这一特性决定了我们在C#代码中不能只保存一个静态token,而是要实现缓存和过期自动刷新。
public class FeishuAppConfig
{
public string AppId { get; set; }
public string AppSecret { get; set; }
public string BaseUrl { get; set; } = "https://open.feishu.cn/open-apis";
}
把配置项放在单独的类中,可以方便后续从配置文件或环境变量读取。AppId和AppSecret最好不要硬编码在源码里,实际项目中建议使用User Secrets或配置中心管理。下面所有代码都会通过FeishuAppConfig实例注入凭证,保证安全性和可测试性。
二、获取tenant_access_token并实现缓存刷新
飞书API的鉴权思路并不复杂:先使用App ID和App Secret向token接口发送POST请求,响应中会包含tenant_access_token字段和expire字段。expire表示token有效秒数,通常是7200秒。拿到token后,后续所有API请求的HTTP头中都要带上Authorization,值为Bearer加空格加token字符串。
在实际项目中,每次调用API都重新获取token显然不划算,但保存一个全局静态token又可能在两小时后失效。比较稳妥的做法是封装一个FeishuClient类,内部维护token缓存和过期时间。每次调用业务接口前先检查token是否过期,如果过期则重新请求,否则直接复用。这样既减少了token请求次数,又避免了手动管理有效期的麻烦。
public class FeishuClient
{
private readonly HttpClient _httpClient;
private readonly FeishuAppConfig _config;
private string _tenantAccessToken;
private DateTime _tokenExpireTime = DateTime.MinValue;
public FeishuClient(FeishuAppConfig config)
{
_config = config;
_httpClient = new HttpClient();
_httpClient.BaseAddress = new Uri(config.BaseUrl);
}
private async Task<string> GetTenantAccessTokenAsync()
{
if (!string.IsNullOrEmpty(_tenantAccessToken) && DateTime.Now < _tokenExpireTime)
{
return _tenantAccessToken;
}
var requestBody = new
{
app_id = _config.AppId,
app_secret = _config.AppSecret
};
var json = Newtonsoft.Json.JsonConvert.SerializeObject(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
var response = await _httpClient.PostAsync("/auth/v3/tenant_access_token/internal", content);
response.EnsureSuccessStatusCode();
var responseJson = await response.Content.ReadAsStringAsync();
var tokenResponse = Newtonsoft.Json.JsonConvert.DeserializeObject<FeishuTokenResponse>(responseJson);
if (tokenResponse.Code != 0)
{
throw new Exception($"获取token失败: {tokenResponse.Msg}");
}
_tenantAccessToken = tokenResponse.TenantAccessToken;
_tokenExpireTime = DateTime.Now.AddSeconds(tokenResponse.Expire - 60);
return _tenantAccessToken;
}
}
上面的代码中,token提前60秒过期,这是为了防止服务器时间与飞书服务器时间存在细微偏差。FeishuTokenResponse类是响应JSON的反序列化模型,包含Code、Msg、TenantAccessToken和Expire四个属性。这个类也可以直接定义在FeishuClient内部,避免暴露过多类型。
还需要注意的是,HttpClient如果频繁创建和销毁,可能会造成套接字耗尽。FeishuClient在构造函数中创建了一个HttpClient实例,并在整个生命周期内复用,这是一个良好的实践。如果你的项目使用依赖注入,建议把FeishuClient注册为单例,同时保证线程安全。当前实现中token缓存字段没有加锁,对于高并发场景,可以加上SemaphoreSlim或使用MemoryCache。
public class FeishuTokenResponse
{
public int Code { get; set; }
public string Msg { get; set; }
public string TenantAccessToken { get; set; }
public int Expire { get; set; }
}
三、发送文本消息与交互式卡片消息
拿到token之后,就可以调用飞书的消息发送接口。文本消息是最基础的类型,适合用来推送简单的告警或通知。接口地址为POST /im/v1/messages?receive_id_type=open_id,请求体中需要指定接收者ID、消息类型和消息内容。对于机器人在群聊中发消息,接收者ID通常是chat_id,此时receive_id_type参数应设置为chat_id。
发送文本消息时,content字段是一个JSON字符串,即使它看起来像对象,也必须以字符串形式传递。例如发送一条内容是“服务异常,请检查日志”的消息,content的值应为"{\"text\":\"服务异常,请检查日志\"}"。很多开发者在这里容易直接把匿名对象序列化后放进content,导致飞书返回参数错误。下面封装一个SendTextMessageAsync方法,内部正确处理序列化逻辑。
public async Task SendTextMessageAsync(string receiveId, string receiveIdType, string text)
{
var token = await GetTenantAccessTokenAsync();
var requestBody = new
{
receive_id = receiveId,
msg_type = "text",
content = Newtonsoft.Json.JsonConvert.SerializeObject(new { text = text })
};
var json = Newtonsoft.Json.JsonConvert.SerializeObject(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
using var request = new HttpRequestMessage(HttpMethod.Post, $"/im/v1/messages?receive_id_type={receiveIdType}");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
request.Content = content;
var response = await _httpClient.SendAsync(request);
var responseJson = await response.Content.ReadAsStringAsync();
var result = Newtonsoft.Json.JsonConvert.DeserializeObject<FeishuBaseResponse>(responseJson);
if (result.Code != 0)
{
throw new Exception($"发送消息失败: {result.Msg}");
}
}
交互式卡片消息比纯文本消息更适合展示结构化内容,例如告警详情、工单信息或操作按钮。卡片消息的content同样是JSON字符串,不过结构要复杂得多。飞书卡片使用一套类似前端组件的JSON Schema,支持标题、分割线、字段列表、按钮等元素。构造卡片时,建议先在飞书卡片搭建工具中调试,确认布局符合预期后再把JSON保存到代码中。
下面是一个简单的卡片消息示例,包含标题、一个文本字段和一个跳转按钮。实际使用时,可以根据业务需求动态替换字段内容。卡片JSON中如果包含变量,建议使用字符串插值或模板引擎,但要小心JSON转义。
public async Task SendCardMessageAsync(string receiveId, string receiveIdType, string title, string alertContent)
{
var token = await GetTenantAccessTokenAsync();
var card = new
{
config = new { wide_screen_mode = true },
header = new
{
title = new { tag = "plain_text", content = title },
template = "blue"
},
elements = new object[]
{
new
{
tag = "div",
text = new
{
tag = "lark_md",
content = alertContent
}
},
new
{
tag = "action",
actions = new object[]
{
new
{
tag = "button",
text = new { tag = "plain_text", content = "查看详情" },
type = "primary",
url = "https://www.feishu.cn"
}
}
}
}
};
var requestBody = new
{
receive_id = receiveId,
msg_type = "interactive",
content = Newtonsoft.Json.JsonConvert.SerializeObject(card)
};
var json = Newtonsoft.Json.JsonConvert.SerializeObject(requestBody);
var content = new StringContent(json, Encoding.UTF8, "application/json");
using var request = new HttpRequestMessage(HttpMethod.Post, $"/im/v1/messages?receive_id_type={receiveIdType}");
request.Headers.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", token);
request.Content = content;
var response = await _httpClient.SendAsync(request);
var responseJson = await response.Content.ReadAsStringAsync();
var result = Newtonsoft.Json.JsonConvert.DeserializeObject<FeishuBaseResponse>(responseJson);
if (result.Code != 0)
{
throw new Exception($"发送卡片消息失败: {result.Msg}");
}
}
发送卡片消息时,receive_id_type的值仍然可以是open_id、user_id、email或chat_id,具体取决于业务场景。如果是机器人主动向群聊推送,使用chat_id最直接。群聊的chat_id可以在飞书客户端群设置中查看,也可以通过飞书开放平台的事件回调获取。
四、事件回调验签与被动响应
如果希望飞书服务器主动把事件推送给C#服务,例如用户@机器人、机器人被拉入群聊、消息被撤回等,就需要在开放平台配置事件订阅的请求地址。飞书会向该地址发送POST请求,请求体包含事件类型和事件数据。为了确保请求确实来自飞书,回调地址必须实现验签逻辑。飞书目前支持两种验证方式:请求头中的X-Lark-Signature签名验证和Encrypt Key加密验证。
验签的核心思路是:使用应用配置中的Verification Token作为密钥,对请求体原文计算HMAC-SHA256哈希,将结果进行Base64编码后与请求头中的X-Lark-Signature值比对。如果一致则说明请求可信。Verification Token可以在飞书开放平台的事件订阅页面获取。在C#中实现这个逻辑并不复杂,但需要注意读取请求体时不能直接将StreamReader用于验签后再读取一次,因为请求体只能读取一次。正确做法是先读取原始字符串,再根据该字符串计算签名。
public bool VerifySignature(string requestBody, string signature, string verificationToken)
{
if (string.IsNullOrEmpty(signature) || string.IsNullOrEmpty(verificationToken))
{
return false;
}
using var hmac = new System.Security.Cryptography.HMACSHA256(Encoding.UTF8.GetBytes(verificationToken));
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(requestBody));
var computedSignature = Convert.ToBase64String(hash);
return string.Equals(computedSignature, signature, StringComparison.Ordinal);
}
在ASP.NET Core的WebAPI中,可以从HttpContext中读取请求体,然后调用VerifySignature方法。验签失败时应返回401状态码,验签成功后才进行事件处理。如果事件类型是URL验证,飞书会发送一个challenge字段,服务端需要在响应体中原样返回该字段。对于其他事件类型,飞书要求返回HTTP 200并在响应体中包含code为0的JSON,表示事件已接收。如果响应超时或返回错误,飞书会进行一定次数的重试。
[HttpPost("feishu/event")]
public async Task<IActionResult> HandleFeishuEvent()
{
using var reader = new StreamReader(Request.Body, Encoding.UTF8);
var requestBody = await reader.ReadToEndAsync();
var signature = Request.Headers["X-Lark-Signature"].ToString();
var verificationToken = _config.VerificationToken;
if (!_feishuClient.VerifySignature(requestBody, signature, verificationToken))
{
return Unauthorized();
}
var eventData = Newtonsoft.Json.JsonConvert.DeserializeObject<FeishuEventRequest>(requestBody);
if (eventData.Type == "url_verification")
{
return Ok(new { challenge = eventData.Challenge });
}
// 处理其他事件类型
return Ok(new { code = 0 });
}
事件回调的验签是最容易被忽略但也最容易导致安全问题的环节。如果不对请求来源进行验证,任何人都可以伪造飞书事件调用你的接口,从而触发错误的业务逻辑。即使是内部系统,也建议从上线开始就启用验签,而不是等出现安全事件后再补救。
五、封装建议与常见错误排查
把飞书API对接逻辑封装成独立类库后,可以统一管理token、请求头和异常处理。建议至少定义两个基础响应类:FeishuBaseResponse用于只有code和msg的接口,FeishuTokenResponse继承或单独定义。对于不同的业务接口,可以在FeishuClient中增加对应方法,保持单一职责。如果项目中有多个飞书应用,也可以把FeishuClient改造成支持多租户的工厂模式。
常见错误主要集中在以下几个方面:第一,content字段没有正确序列化为字符串,导致msg_type和content类型不匹配;第二,receive_id_type参数与receive_id实际类型不一致,比如用open_id却传了chat_id;第三,token过期但代码中仍然使用旧值,报错code为99991663或99991664;第四,回调验签失败,通常是因为请求体被提前读取或Verification Token配置错误。遇到这些错误时,可以优先检查请求日志,把飞书返回的code和msg完整记录下来。
为了便于排查问题,可以在FeishuClient中增加一个日志回调,在每次请求前记录URL、请求头和请求体,在响应后记录状态码和响应体。飞书返回的错误信息通常比较明确,结合日志很快就能定位原因。如果需要在生产环境中监控token刷新频率和API调用成功率,还可以把这些指标接入现有的监控系统。
- 确保App Secret和Verification Token不要提交到公开仓库
- 所有API调用使用异步方法,避免阻塞线程
- 在发送消息前校验接收者ID是否有效,减少无效请求
- 为卡片消息维护版本号,方便后续调整布局
掌握这些基础封装和调试技巧后,C#对接飞书API的剩余工作就只是根据业务需求组合不同的飞书接口。无论是推送告警、同步审批流,还是构建更复杂的机器人交互,核心的鉴权和请求处理逻辑都可以复用本文中的FeishuClient模式。
C#飞书API飞书API对接tenant_access_token修改时间:2026-09-20 06:19:47