Spring Boot 集成飞书推送超详细教程:文本消息、签名校验、封装工具类一篇搞定

在企业项目里,消息通知几乎是刚需。

比如订单支付成功要通知运营,接口报错要通知开发,定时任务执行失败要通知值班人员,服务器告警也要第一时间推到群里。很多团队第一反应是发短信、发邮件,但真到了落地阶段,飞书群消息反而更直接,成本更低,接入也更快。

如果你的项目是 Spring Boot,那么最适合快速落地的方案,通常就是接入飞书自定义机器人 。它本质上是一个群聊级别的 Webhook,你的服务端只需要向这个地址发送 HTTP POST 请求,就可以把文本、富文本、图片甚至卡片消息推送到飞书群中。飞书官方文档也明确说明了,自定义机器人适合群聊消息推送,通过 webhook 地址即可发送多种类型消息;如果你需要更复杂的系统集成能力,则通常要考虑"应用机器人"方案。(飞书开放平台)

这篇文章我不讲虚的,直接带你从 0 到 1 完成 Spring Boot 集成飞书推送,包括:

  1. 飞书机器人怎么创建

  2. Spring Boot 项目怎么配置

  3. 文本消息怎么发

  4. 开启"签名校验"之后怎么处理

  5. 怎么封装成企业项目里可复用的工具类

  6. 常见报错怎么排查

  7. 最后给你一个可以直接上线的实战代码结构


一、为什么企业项目里推荐先接飞书自定义机器人

对于大多数"群通知"场景,比如异常告警、业务提醒、订单通知、审批提醒,自定义机器人已经足够用了。它的优点很明显:

  • 接入简单,不需要你先做一整套飞书应用体系

  • 只要拿到 webhook 地址,就能直接发消息

  • 支持文本、富文本、图片、群名片、交互式消息卡片等消息类型

  • 很适合做项目里的"告警通知组件"或者"消息推送基础设施"

飞书官方说明中提到,自定义机器人可以直接通过 webhook 地址推送消息,支持文本、富文本、图片等多种类型;而"应用机器人"更偏向开放接口调用、复杂系统集成。(飞书开放平台)

所以如果你这篇文章是写给 CSDN 读者看,我建议你把主线聚焦在:

Spring Boot + 飞书自定义机器人 + Webhook 推送

这样最实用,也最容易让读者复现。


二、接入前先搞清楚两种方案

很多人一上来就会混淆"自定义机器人"和"应用机器人",这里先给你讲透。

1. 自定义机器人

它是直接在飞书群里添加的,拿到一个 webhook 地址后,你的服务端就能往群里发消息。适合快速通知、监控报警、业务提醒。(飞书开放平台)

2. 应用机器人

它是在飞书开放平台创建应用,然后给应用开通机器人能力,可以调用更多 OpenAPI,适合更完整的业务集成。飞书官方的机器人概述也把这两者做了区分:应用机器人适合系统集成,自定义机器人更适合群聊消息推送。(飞书开放平台)

本文选择的是最容易落地的方案:自定义机器人。


三、先在飞书里创建自定义机器人

这一步不写清楚,很多人连 webhook 都拿不到。

操作步骤

  1. 打开飞书客户端

  2. 进入你要接收通知的群聊

  3. 打开群设置

  4. 找到"群机器人"

  5. 选择"添加机器人"

  6. 选择"自定义机器人"

  7. 填写机器人名称和描述

  8. 创建成功后复制 webhook 地址

飞书官方 FAQ 说明,自定义机器人需要在飞书客户端的指定群聊内添加;不是去开发者后台建。(飞书开放平台)

创建完成后,你通常还能看到几项安全设置,比如:

  • 自定义关键词

  • IP 白名单

  • 签名校验

飞书官方文档明确提到,自定义机器人支持 IP 白名单和签名校验等安全设置,用来保证调用安全。(飞书开放平台)


四、Spring Boot 项目准备

下面我们开始写代码。

1. Maven 依赖

这里我建议用 spring-boot-starter-web,再配合 Jackson 做 JSON 序列化即可。为了方便写 HTTP 请求,也可以直接用 RestTemplate。如果你项目里已经在用 hutoolokhttp,也可以替换。

复制代码
<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Lombok,可选 -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- 单元测试,可选 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

五、配置文件怎么写

一般我们会把 webhook 和签名密钥放到配置文件里。

application.yml

复制代码
feishu:
  webhook: https://open.feishu.cn/open-apis/bot/v2/hook/你的webhook
  enabled: true
  secret: 你的签名密钥

说明一下:

  • webhook:飞书机器人地址

  • enabled:是否启用,方便区分测试环境和生产环境

  • secret:如果你开启了签名校验,这里要配置对应密钥;如果没开启,可以为空


六、先定义配置类

复制代码
package com.example.demo.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "feishu")
public class FeishuProperties {

    /**
     * 机器人 webhook 地址
     */
    private String webhook;

    /**
     * 是否启用
     */
    private Boolean enabled = true;

    /**
     * 签名密钥,可为空
     */
    private String secret;
}

七、先跑通最基础的文本消息推送

飞书自定义机器人发送消息,本质上就是向 webhook 发一个 POST 请求。常见的消息体里会带 msg_type,文本消息一般对应 text 类型。飞书官方文档中也给出了自定义机器人支持的消息类型,包含文本、富文本、图片等;在相关资料里也能看到 msg_type 常用取值包括 textpostimageinteractive 等。(飞书开放平台)

1. 定义请求对象

复制代码
package com.example.demo.feishu.dto;

import lombok.Data;

@Data
public class FeishuTextRequest {

    private String msg_type;
    private TextContent content;

    @Data
    public static class TextContent {
        private String text;
    }
}

2. 编写发送服务

复制代码
package com.example.demo.feishu.service;

import com.example.demo.config.FeishuProperties;
import com.example.demo.feishu.dto.FeishuTextRequest;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

@Service
@RequiredArgsConstructor
public class FeishuBotService {

    private final FeishuProperties feishuProperties;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final RestTemplate restTemplate = new RestTemplate();

    public String sendText(String text) {
        if (Boolean.FALSE.equals(feishuProperties.getEnabled())) {
            return "飞书推送未启用";
        }

        FeishuTextRequest request = new FeishuTextRequest();
        request.setMsg_type("text");

        FeishuTextRequest.TextContent content = new FeishuTextRequest.TextContent();
        content.setText(text);
        request.setContent(content);

        return doPost(request);
    }

    private String doPost(Object requestBody) {
        try {
            String json = objectMapper.writeValueAsString(requestBody);

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            HttpEntity<String> entity = new HttpEntity<>(json, headers);

            ResponseEntity<String> response = restTemplate.postForEntity(
                    feishuProperties.getWebhook(),
                    entity,
                    String.class
            );

            return response.getBody();
        } catch (JsonProcessingException e) {
            throw new RuntimeException("飞书消息序列化失败", e);
        } catch (Exception e) {
            throw new RuntimeException("飞书消息发送失败", e);
        }
    }
}

3. 写一个测试接口

复制代码
package com.example.demo.controller;

import com.example.demo.feishu.service.FeishuBotService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/feishu")
@RequiredArgsConstructor
public class FeishuTestController {

    private final FeishuBotService feishuBotService;

    @GetMapping("/send")
    public String send(@RequestParam String msg) {
        return feishuBotService.sendText(msg);
    }
}

启动项目后访问:

复制代码
http://localhost:8080/feishu/send?msg=SpringBoot接入飞书成功

如果群里成功收到消息,说明最基础的链路已经打通了。


八、企业项目里一定要加签名校验

上面只是最简版。

真正线上使用时,我强烈建议你把飞书机器人的签名校验 打开。因为 webhook 一旦泄露,别人就可能往你的群里乱发消息。飞书官方文档明确提到,自定义机器人支持签名校验;开启后,请求中需要带上 timestampsign 字段。其签名规则是:将 timestamp + "\n" + 密钥 作为签名字符串,使用 HmacSHA256 算法计算,再做 Base64 编码。(飞书开放平台)

这一步非常关键,也是很多教程写得不够清楚的地方。


九、签名算法怎么写

1. 定义带签名的通用请求对象

复制代码
package com.example.demo.feishu.dto;

import lombok.Data;

@Data
public class FeishuSignedRequest<T> {

    /**
     * 时间戳
     */
    private String timestamp;

    /**
     * 签名
     */
    private String sign;

    /**
     * 消息类型
     */
    private String msg_type;

    /**
     * 消息内容
     */
    private T content;
}

2. 编写签名工具类

复制代码
package com.example.demo.feishu.util;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;

public class FeishuSignUtil {

    private static final String HMAC_SHA256 = "HmacSHA256";

    public static String genSign(String timestamp, String secret) {
        try {
            String stringToSign = timestamp + "\n" + secret;
            Mac mac = Mac.getInstance(HMAC_SHA256);
            SecretKeySpec spec = new SecretKeySpec(
                    stringToSign.getBytes(StandardCharsets.UTF_8),
                    HMAC_SHA256
            );
            mac.init(spec);
            byte[] signData = mac.doFinal(new byte[]{});
            return Base64.getEncoder().encodeToString(signData);
        } catch (Exception e) {
            throw new RuntimeException("生成飞书签名失败", e);
        }
    }
}

3. 改造发送服务

复制代码
package com.example.demo.feishu.service;

import com.example.demo.config.FeishuProperties;
import com.example.demo.feishu.dto.FeishuSignedRequest;
import com.example.demo.feishu.util.FeishuSignUtil;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class FeishuBotService {

    private final FeishuProperties feishuProperties;
    private final ObjectMapper objectMapper = new ObjectMapper();
    private final RestTemplate restTemplate = new RestTemplate();

    public String sendText(String text) {
        if (Boolean.FALSE.equals(feishuProperties.getEnabled())) {
            return "飞书推送未启用";
        }

        Map<String, String> content = new HashMap<>();
        content.put("text", text);

        return send("text", content);
    }

    public String send(String msgType, Object content) {
        try {
            String timestamp = String.valueOf(System.currentTimeMillis() / 1000);

            FeishuSignedRequest<Object> request = new FeishuSignedRequest<>();
            request.setMsg_type(msgType);
            request.setContent(content);

            if (StringUtils.hasText(feishuProperties.getSecret())) {
                request.setTimestamp(timestamp);
                request.setSign(FeishuSignUtil.genSign(timestamp, feishuProperties.getSecret()));
            }

            String json = objectMapper.writeValueAsString(request);

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);

            HttpEntity<String> entity = new HttpEntity<>(json, headers);

            ResponseEntity<String> response = restTemplate.postForEntity(
                    feishuProperties.getWebhook(),
                    entity,
                    String.class
            );

            return response.getBody();
        } catch (JsonProcessingException e) {
            throw new RuntimeException("飞书消息序列化失败", e);
        } catch (Exception e) {
            throw new RuntimeException("飞书消息发送失败", e);
        }
    }
}

十、发送富文本消息

很多读者写完文本消息就结束了,但真实项目里,纯文本往往不够用。比如你想发:

  • 项目名

  • 环境名

  • 错误时间

  • 错误堆栈

  • 点击链接查看详情

这时候富文本更适合。

飞书官方文档中说明,自定义机器人支持富文本消息;相关说明中 msg_type 可使用 post。(飞书开放平台)

示例代码

复制代码
public String sendPost() {
    Map<String, Object> zhCn = new HashMap<>();
    zhCn.put("title", "系统告警通知");

    Object[][] content = new Object[][]{
            {
                    Map.of("tag", "text", "text", "服务名:订单服务")
            },
            {
                    Map.of("tag", "text", "text", "级别:ERROR")
            },
            {
                    Map.of("tag", "text", "text", "时间:2026-03-26 10:30:00")
            },
            {
                    Map.of("tag", "a", "text", "点击查看详情", "href", "https://your-domain.com/log/detail/1001")
            }
    };

    zhCn.put("content", content);

    Map<String, Object> post = new HashMap<>();
    post.put("zh_cn", zhCn);

    return send("post", post);
}

这个结构本质上就是告诉飞书:

我发送的是 post 富文本消息,语言版本是 zh_cn,标题是"系统告警通知",正文里每一行放哪些组件。


十一、发送卡片消息

如果你只是做普通通知,文本和富文本就够了。但如果你想把消息做得更像"运维告警面板"或者"审批通知卡片",那就可以用消息卡片

飞书官方文档说明,自定义机器人也支持发送卡片消息;同时飞书卡片是一种结构化内容载体,适合做信息展示和轻量交互。(飞书开放平台)

简单卡片示例

复制代码
public String sendCard() {
    Map<String, Object> header = Map.of(
            "title", Map.of("tag", "plain_text", "content", "订单支付通知"),
            "template", "blue"
    );

    Map<String, Object> element1 = Map.of(
            "tag", "div",
            "text", Map.of(
                    "tag", "lark_md",
                    "content", "**订单号:** 202603260001\n**支付状态:** 成功\n**支付金额:** ¥99.00"
            )
    );

    Map<String, Object> element2 = Map.of(
            "tag", "action",
            "actions", new Object[]{
                    Map.of(
                            "tag", "button",
                            "text", Map.of("tag", "plain_text", "content", "查看订单"),
                            "type", "primary",
                            "url", "https://your-domain.com/order/detail/202603260001"
                    )
            }
    );

    Map<String, Object> card = new HashMap<>();
    card.put("header", header);
    card.put("elements", new Object[]{element1, element2});

    return send("interactive", card);
}

这个效果会比普通文本好很多,特别适合:

  • 订单通知

  • 发布提醒

  • 服务器告警

  • 审批消息

  • 定时任务执行结果


十二、把它封装成项目里的公共能力

如果你是做企业项目,千万别把飞书推送代码散落在各个业务类里。正确做法是做一层统一封装。

我建议你这样分层:

复制代码
com.example.demo
├── config
│   └── FeishuProperties.java
├── feishu
│   ├── dto
│   │   ├── FeishuTextRequest.java
│   │   └── FeishuSignedRequest.java
│   ├── service
│   │   └── FeishuBotService.java
│   └── util
│       └── FeishuSignUtil.java
└── controller
    └── FeishuTestController.java

如果你的项目已经比较大了,可以进一步抽成:

  • message-api:定义消息接口

  • message-feishu:飞书实现

  • message-wechat:企业微信或微信公众号实现

  • message-email:邮件实现

这样后续替换通知渠道时,不需要改业务代码,只要改消息实现。


十三、推荐你在业务里怎么用

飞书推送最常见的使用方式,不是给前台用户发消息,而是给内部团队通知

1. 异常告警

复制代码
try {
    // 业务代码
} catch (Exception e) {
    feishuBotService.sendText("【系统异常】订单服务发生异常:" + e.getMessage());
    throw e;
}

2. 支付成功通知

复制代码
feishuBotService.sendText(
    "【支付通知】用户 10001 支付成功,订单号:202603260001,金额:99.00 元"
);

3. 定时任务结果通知

复制代码
feishuBotService.sendText(
    "【定时任务】会员过期扫描任务执行完成,处理 125 条记录,耗时 3.2 秒"
);

4. 运维告警通知

复制代码
feishuBotService.sendText(
    "【告警】生产环境 Redis 连接数过高,请立即排查"
);

十四、频控问题一定要知道

这个点非常重要,很多人线上一出问题,日志炸了,然后代码疯狂往飞书推送,最后直接触发限流。

飞书官方文档里给出了自定义机器人的频控规则:单租户单机器人 100 次/分钟,5 次/秒 。超限后消息会发送失败。(飞书开放平台)

所以线上建议你至少做两层保护:

1. 本地限流

例如同一种错误 1 分钟内只推一次。

2. 去重推送

同一异常堆栈不要连续推送 100 条。

3. 异步发送

不要在主业务线程里同步卡住,尤其是高并发接口。

4. 告警聚合

把 1 分钟内的同类错误汇总后一次发送。

这一步做了,你的飞书告警体系才算真正能用。


十五、常见报错排查

下面我把最常见的问题直接给你总结出来。

1. webhook 地址错误

表现:请求发出去了,但飞书群没收到。

排查:检查 webhook 是否复制完整,是否多了空格。

2. 没开机器人

表现:一直返回失败。

排查:确认机器人已经成功添加到目标群。

3. 签名错误

表现:开启签名校验后发送失败。

排查重点:

  • timestamp 是否传了

  • sign 是否传了

  • 签名算法是否正确

  • 密钥是否和飞书后台一致

  • 时间戳是否使用秒级,而不是毫秒级

飞书官方文档明确要求,开启签名验证后,请求中需要附带 timestampsign;签名算法基于 timestamp + "\n" + 密钥,再使用 HmacSHA256 和 Base64 处理。(飞书开放平台)

4. 触发频控

表现:前几条正常,后面突然发送失败。

排查:检查是否短时间内推送过多消息。官方对自定义机器人有明确频控要求。(飞书开放平台)

5. JSON 结构不正确

表现:HTTP 200 但飞书不展示,或者接口直接报参数错误。

排查:检查 msg_typecontent 结构是否匹配。文本、富文本、卡片对应的 JSON 结构不一样。飞书官方文档对不同消息类型的内容结构有专门说明。(飞书开放平台)


十六、生产环境最佳实践

如果你想把这篇文章写得更"像大厂实战",这一段一定要保留。

1. webhook 不要硬编码

统一放到配置中心,或者至少放到环境变量里。

2. 不同环境分不同机器人

开发环境、测试环境、生产环境分别使用不同群,不然测试消息会污染生产群。

3. 做统一消息网关

不要在订单模块、支付模块、会员模块各写一套飞书代码。统一做成 MessageService

4. 失败重试要克制

网络抖动可以重试,但不要无限重试,更不要在高频异常场景中疯狂补发,否则会更容易命中频控。

5. 敏感信息脱敏

错误通知里不要直接发用户身份证、手机号、token、密钥等敏感信息。

6. 告警要分级

  • INFO:普通业务通知

  • WARN:业务异常提醒

  • ERROR:系统错误

  • FATAL:核心服务不可用

建议不同级别走不同群,或者不同消息模板。


十七、完整工具类版本

如果你想让读者复制就能用,那最好给一个整合版。下面这个版本可以直接作为公共组件放进项目。

复制代码
package com.example.demo.feishu.service;

import com.example.demo.config.FeishuProperties;
import com.example.demo.feishu.util.FeishuSignUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class FeishuBotService {

    private final FeishuProperties feishuProperties;
    private final RestTemplate restTemplate = new RestTemplate();
    private final ObjectMapper objectMapper = new ObjectMapper();

    public String sendText(String text) {
        Map<String, String> content = new HashMap<>();
        content.put("text", text);
        return send("text", content);
    }

    public String sendPost(String title, Object[][] contentArray) {
        Map<String, Object> zhCn = new HashMap<>();
        zhCn.put("title", title);
        zhCn.put("content", contentArray);

        Map<String, Object> post = new HashMap<>();
        post.put("zh_cn", zhCn);

        return send("post", post);
    }

    public String sendCard(Map<String, Object> card) {
        return send("interactive", card);
    }

    public String send(String msgType, Object content) {
        if (Boolean.FALSE.equals(feishuProperties.getEnabled())) {
            return "飞书推送未启用";
        }

        try {
            Map<String, Object> body = new HashMap<>();
            body.put("msg_type", msgType);
            body.put("content", content);

            if ("interactive".equals(msgType)) {
                body.remove("content");
                body.put("card", content);
            }

            if (StringUtils.hasText(feishuProperties.getSecret())) {
                String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
                String sign = FeishuSignUtil.genSign(timestamp, feishuProperties.getSecret());
                body.put("timestamp", timestamp);
                body.put("sign", sign);
            }

            String json = objectMapper.writeValueAsString(body);

            HttpHeaders headers = new HttpHeaders();
            headers.setContentType(MediaType.APPLICATION_JSON);
            HttpEntity<String> entity = new HttpEntity<>(json, headers);

            ResponseEntity<String> response = restTemplate.postForEntity(
                    feishuProperties.getWebhook(),
                    entity,
                    String.class
            );

            return response.getBody();
        } catch (Exception e) {
            throw new RuntimeException("飞书推送失败", e);
        }
    }
}

十八、这篇文章的总结

如果你是第一次做 Spring Boot 集成飞书推送,你只要记住下面这条主线就够了:

创建飞书自定义机器人 → 拿到 webhook → Spring Boot 发 HTTP POST → 文本消息先跑通 → 再加签名校验 → 最后封装成公共通知组件。

飞书官方资料表明,自定义机器人非常适合群聊消息推送,通过 webhook 即可发送多种消息类型;而开启签名校验后,需要额外带上 timestampsign,并遵守相应频控规则。(飞书开放平台)

对于大多数 Spring Boot 项目来说,这种接入方式已经足够应对:

  • 接口异常告警

  • 支付结果通知

  • 订单状态提醒

  • 审批流程通知

  • 定时任务执行结果推送

  • 运维报警

相关推荐
二十雨辰3 小时前
[Java]-场景题
java·开发语言
MZA6663 小时前
Spring Boot 实战:基于自定义注解 + AOP 实现 Kafka 消息无侵入异步上送
java·spring boot·kafka
我是大猴子3 小时前
为什么SSE无法代替Webscoket
java
Elastic 中国社区官方博客3 小时前
隐藏在可观测性数据中的安全攻击
大数据·网络·安全·elasticsearch·搜索引擎·全文检索
PM老周3 小时前
团队知识库软件哪个好用?ONES、Confluence、Notion、飞书、语雀等主流工具对比
飞书·confluence·ones·知识库管理
wno7043 小时前
Spring Cloud Hystrix Dashboard仪表盘
spring·spring cloud·hystrix
重庆小透明3 小时前
Kafka 完全指南:从基础组件到核心原理(包含面试题)
java·分布式·微服务·架构·kafka
CodeStats4 小时前
【Java文件IO】Java 文件 IO 体系深度拆解(中):NIO API 详解、Files 与 FileChannel 巅峰对决
java·io·文件·nio·零拷贝·mmap
考虑考虑4 小时前
Redis8.8新特性
运维·redis·后端
见闻小天地4 小时前
空调机房冷冻泵与冷却泵是什么?选型要点与赛莱默方案解析
大数据·网络