导读:本期聚焦于阿狸创作的《C#如何对接飞书API?从鉴权到消息推送的完整教程与代码实例》,敬请观看详情。想把飞书机器人消息推送嵌入到C#业务系统,最先要解决的不是发送请求,而是理清开放平台的鉴权链路和token刷新机制。本文以飞书自建应用为例,演示如何用HttpClient完成tenant_access_token获取、文本消息和交互式卡片消息推送,并给出事件订阅回调的签名校验思路。代码基于.NET 8和Newtonsoft.Json,可直接集成到现有WebAPI或Worker服务中。为了避免固定token过期导致调用失败,还封装了一个带缓存和自动刷新的FeishuClient类。通过创建应用、配置权限、调用接口、处理回调四个步骤,你能快速跑通C#对接飞书API的完整流程,并把可复用代码直接放进实际项目里使用。

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

C#如何对接飞书API?从鉴权到消息推送的完整教程与代码实例

一、创建飞书自建应用并配置权限

飞书开放平台提供了两种常见的应用类型:企业自建应用和商店应用。对于内部系统集成来说,自建应用是最直接的选择。登录飞书开放平台后台,创建一个企业自建应用,创建完成后会得到两个核心凭证: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

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0920/59537.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。