iT邦幫忙

2025 iThome 鐵人賽

DAY 3
2
Modern Web

Line Bot × NestJS:30 天開發日記系列 第 3

Day 3:Webhook 簽章驗證與訊息回覆策略(Push & Reply)

  • 分享至 

  • xImage
  •  

2025 鐵人賽背景圖

前言

上一篇帶大家透過「Webhook 模式」成功架設了專屬的 LINE Bot 代理人。相信大家在體驗基礎功能後,對於 LINE Bot Webhook 還能監聽哪些事件、支援哪些類型的訊息回覆,以及如何確保後端收到的訊息確實來自 LINE 平台等安全性議題更感興趣。

本文將著重於實現以下核心功能:

  • 介紹 LINE 傳送訊息的兩種方式 Push Message & Reply Message
  • 建立 Webhook 簽章驗證機制確保通訊安全
  • 解析 LINE Webhook 常用事件類型與處理機制

本日程式碼的範例連結

LINE 傳送訊息的兩種方式 Push Message & Reply Message

LINE Bot 回覆使用兩種方式

Push Message(主動推播訊息)

  • Bot 可主動發送訊息給用戶,例如:提醒通知、訂閱推播
  • 可選擇針對單個用戶、多個用戶或是廣播的方式推播訊息

LINE Official Account 方案說明(不同方案會有不同的免費訊息推播限額)

  • 主動推播每一位用戶都會消耗 1 則免費訊息的額度。
    例如:群組 50 個人,推播一次就會消耗 50 則免費訊息額度。
  • 超過計畫中的免費訊息額度時,只有高用量可以發送額外訊息
    LINE BOT Plan
  • Day 1 所提到的【 Chat 模式 】不會消耗推播免費訊息(free messages)的額度

Reply Message(被動回覆訊息)

  • 適合使用在即時對話,例如:使用者詢問問題,Bot 立即回應
  • 必須搭配 Reply Token使用,且只能被使用一次

Reply Token 說明:

  • 保證在收到 Webhook 事件後 1 分鐘之內有效,超過 20 分鐘失效(在這期間不保證有效)
  • 一個 Reply Token 最多可以回覆 5 則訊息
  • 使用 Reply Message 不會消耗免費訊息額度

LINE 平台簽章驗證機制

Day 2 的範例中,我們成功實現了「當使用者傳送訊息時,能夠透過 Webhook 回傳 Hello World!」,但實作過程中卻沒有使用到 Channel Secret,這部分衍生出一個重要的安全議題:

如何確認請求來源確實是 LINE 平台?

如下圖所示,如果第三方惡意服務透過中間人攻擊的方式,攔截到 LINE 傳遞給後端伺服器的資料,是否就能夠任意修改傳入的內容,進而對系統造成威脅?

LINE webhook 官方驗證機制

為了解決這個安全隱憂,LINE 在傳遞事件資訊到後端時,會使用 Channel Secret 對請求內容進行數位簽章,並將產生的簽章放置在 HTTP 請求標頭的 x-line-signature 屬性中。透過這個機制,後端伺服器便能在處理請求之前,先行驗證資料來源是否真的來自 LINE 平台。

接下來我們將分兩個部分說明:

  • 使用 Postman 模擬非 LINE 平台發送訊息,展示缺乏數位簽章驗證的安全風險
  • 透過 LINE 數位簽章驗證請求來源的實作步驟

Postman 模擬非 LINE 平台發送訊息異常情形

Step 1:印出 LINE 平台接收到的事件

用戶使用 LINE 應用程式傳送訊息至架設好的官方帳號,透過終端機查看印出的訊息

// 略
  /**
   * Webhook 端點處理器
   * 接收來自 LINE Platform 的事件通知
   * @param body LINE Platform 傳送的 Webhook 請求本體
   * @returns 處理完成的回應訊息
   */
  @Post('/webhook')
  async handleWebhook(@Body() body: WebhookRequestBody): Promise<string> {
    console.log(
      'Webhook 從 line platform 接收到的資訊:',
      JSON.stringify(body, null, 2),
    );
    // 略
    
    return 'Webhook processed successfully';
  }

終端機印出的訊息:
用戶透過 LINE 平台發送訊息透過後端伺服器印出

Step 2:使用 Postman 直接打自己後端伺服器的 webhook 路由

複製 Step 1 後端伺服器接收到 LINE 平台事件訊息,模擬第三方駭客擷取了 LINE 平台發送給後端伺服器的資訊,並且將內容進行串改在傳送給後端伺服器。

顯示Webhook processed successfully代表發送至後端的請求處理成功

模擬第三方駭客擷取資訊的情形

使用 LINE 數位簽章驗證請求來源是 LINE 平台

Step 1:修改 main.ts,保留原始 JSON

預設情況下,NestJS 會將傳入的請求資料自動轉換成 JSON 格式,再傳遞到後續的路由處理器中。然而,在進行 LINE 數位簽章驗證時,必須使用原始的 Buffer 資料進行驗證,而非經過解析的 JSON 物件。因此我們需要針對 /webhook 路由保留原始資料格式。

原始資料轉換成 json 的部分,可以想像在 express 執行 express.json() 的效果

main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import * as express from 'express';

async function bootstrap() {
  const app = await NestFactory.create(AppModule, {
    bodyParser: false,
  });
  app.use('/webhook', express.raw({ type: 'application/json' }));
  app.use(express.json());
  await app.listen(process.env.PORT ?? 3000);
}
bootstrap();

Step 2:建立 LINE middleware 中介層(處理路由前的驗證處理)

執行以下指令創建 middleware 檔案:

創建路徑在 src/middleware/line.middleware.ts

nest g middleware line middleware --flat --no-spec

line middleware.ts

// 略

@Injectable()
export class LineMiddleware implements NestMiddleware {
  constructor(private readonly configService: ConfigService) {}

  use(req: Request, res: Response, next: NextFunction) {
    const channelSecret = this.configService.get<string>('LINE_CHANNEL_SECRET');
 
    if (!channelSecret) {
      console.error('LINE_CHANNEL_SECRET 未設定');
      return res.status(500).send({ status: 'Server error' });
    }

    try {
      const signature = req.headers['x-line-signature'] as string;
      if (!signature) {
        console.error('缺少 X-Line-Signature header');
        return res.status(401).send({ status: 'Unauthorized' });
      }

      const bodyBuffer = req.body;

      // 用原始 Buffer 驗證簽名
      if (!validateSignature(bodyBuffer, channelSecret, signature)) {
        console.error('簽名驗證失敗');
        return res.status(401).send({ status: 'Unauthorized' });
      }

      // 簽名驗證通過後,把 Buffer 轉換回 JSON 物件
      req.body = JSON.parse(bodyBuffer.toString('utf8'));
      // 往下繼續到 Controller
      next();
    } catch (error) {
      console.error('LINE Middleware Error:', error);
      return res.status(401).send({ status: 'Unauthorized' });
    }
  }
}

Step 3:在 app.module 中套用 Middleware

只要經過 /webhook端點的請求,都會需要先透過Step 2 middleware的檢查

app.module

// 略

@Module({
  imports: [ConfigModule.forRoot({ isGlobal: true })],
  controllers: [AppController],
  providers: [LineConfigProvider],
})
export class AppModule implements NestModule {
  configure(consumer: MiddlewareConsumer) {
    // 針對 webhook 路由使用 LINE 中介軟體驗證
    consumer.apply(LineMiddleware).forRoutes('webhook');
  }
}

透過 Channel Secret 可以驗證請求來源確實來自 LINE 平台,防止第三方攔截或篡改內容,確保後端服務的安全性。

昨天我們完成了 Hello World 的回覆,接下來我們試著在使用者加入官方帳號的時候,設計一個招呼語並搭配 Reply Message的方式來歡迎使用者的加入吧!

LINE Bot 一對一聊天常見互動事件 - Message、Follow、Unfollow

單聊的情境上來說常見使用的事件有以下三種:

1. Message Event(訊息事件)

  • 情境:用戶發送任何類型的訊息時觸發
  • 用途:回應用戶訊息,如自動回覆、客服應用等

2. Follow Event(加入好友事件)

  • 情境:用戶首次加入好友或解除封鎖官方帳號時觸發
  • 用途:發送歡迎訊息、新用戶導引、優惠活動

3. Unfollow Event(取消好友事件)

  • 情境:用戶封鎖或刪除官方帳號時觸發
  • 用途:記錄封鎖事件、分析用戶流失

當使用者加入 LINE Bot 時會觸發 Follow Event,此時通常會有一個招呼語功能。在 LINE official account 後台Resonse settings中,Greeting message 是預設開啟的功能,點選「Greeting message settings」綠色連結即可在 GUI 介面上進行設定。

LINE OA Greeting message settings

在 LINE Official Account 後台能夠設置基礎的自動回覆功能,支援多種訊息類型,包括:文字訊息、圖片、影音、卡片模板等。管理者可以設定歡迎訊息僅在好友初次加入時觸發。需要注意的是,如果未設置此功能,當使用者執行封鎖後再解除封鎖的操作時,系統會重新發送 LINE 平台的預設招呼語

LINE Greeting message 設定

然而,如果有更進階的應用情境,例如:需要發送客製化的 Flex Message 卡片,或是在用戶加入官方帳號後將資料存入自己的資料庫系統,就必須透過後端伺服器搭配 Webhook 的方式來進行開發。

以下我們試試看將招呼語的功能,透過 webhook 的方式在自己的後端伺服器設計吧!

Step 1:調整 app.controller.ts 檔案內容 - eventHandler

app.controller.ts 中處理 LINE 平台事件時,建議統一透過 eventHandler 來處理對應的事件,這樣可以避免撰寫多個 if...else 判斷,提升程式碼的可讀性和維護性。

先前只有 MessageEvent 的對應處理,這邊可以加上 FollowEventUnfollowEvent 的處理。

app.controller.ts

/// 略
const eventHandler = {
  message: (event) => this.handleMessageEvent(event),
  follow: (event) => this.handleFollowEvent(event),
  unfollow: (event) => this.handleUnfollowEvent(event),
} satisfies Partial<HandlerMap>;

Step 2:撰寫 FollowEvent 處理方法

提醒要關閉 LINE Official Account 後台招呼語的功能,否則 LINE Official Account 設置會跟你自己建置的後端伺服器會同時觸發招呼語!

這邊舉例上是以回覆文字內容為主,但實際上可以結合更多客製化的應用,後續也會呈現結合資料庫的進階應用!

app.controller.ts

/**
* 用戶首次加入好友或解除封鎖官方帳號時觸發
* @param event 加入好友事件
*/
private async handleFollowEvent(event: FollowEvent): Promise<void> {
const { replyToken } = event;
await this.lineClient.replyMessage({
  replyToken,
  messages: [
    {
      type: 'text',
      text: '恭喜你加入我們的官方帳號!',
    },
  ],
});
}

本日結語

在實作 LINE Bot 功能時,並非所有功能都需要透過 Webhook 自行開發。對於簡單常見的應用場景,例如文字描述搭配圖片介紹等基本互動,可以透過 LINE Official Account Manager 後台進行設置,無需撰寫任何程式碼。這樣的設計理念體現了 LINE Bot 開發的靈活性,讓開發者能夠根據需求選擇最適合的實作方式,即使不使用程式開發,也能透過官方提供的工具達到一定功能水準。


上一篇
Day 2:從 Webhook 到後端,打造你的專屬機器人
下一篇
Day 4:掌握 LINE Bot 七種訊息類型與 NestJS 架構優化
系列文
Line Bot × NestJS:30 天開發日記8
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言