2025-08-05 Line 訂單系統- LINE Webhook → NestJS → 驗證 Signature 的「完整生命週期」

2025-08-05 Line 訂單系統- LINE Webhook → NestJS → 驗證 Signature 的「完整生命週期」

LINE Webhook → NestJS → 驗證 Signature 的「完整生命週期」

你問得非常好,我下面用 LINE Webhook → NestJS → 驗證 Signature 的「完整生命週期」來解釋:
到底是哪一個步驟產生 raw body?
為什麼一定要拿 raw body 才能驗證?
NestJS 什麼時候幫你處理 body?
Signature 驗證究竟是怎麼做?

看完你就會完全懂。


🧩 整體流程(重點:raw body 在哪邊產生?)

1️⃣ 使用者在 LINE 打字 → 傳訊息給 LINE

使用者:

  • 傳文字
  • 傳貼圖
  • 傳圖片
  • 點選 Flex Message 按鈕
  • 任何事件……

都會觸發 LINE → 你的 webhook endpoint


2️⃣ LINE 伺服器收到訊息後,組成 JSON(raw body)

這裡是關鍵:

✔ LINE 會把使用者輸入內容包成一段 JSON

例如:

1
2
3
4
5
6
7
8
9
10
11
{
"events": [
{
"type": "message",
"message": {
"type": "text",
"text": "hello"
}
}
]
}

✔ 然後將「這段 JSON(未解析的 bytes)」

直接用 UTF-8 編碼成 raw body(byte array)

例如:

1
7b 0a 20 20 22 65 76 65 6e 74 ...

這就是 raw body
(還沒有被 NestJS 解成物件)


3️⃣ LINE 使用 raw body + channel secret 計算 HMAC-SHA256 → 得到 signature

LINE 做:

1
signature = HMAC_SHA256(rawBody, channelSecret)

然後把 signature 放進:

1
x-line-signature: <signature>

4️⃣ LINE 將 raw body + header 傳給你的 webhook URL

1
2
3
4
5
POST https://xxxx.ngrok-free.app/line/webhook
headers:
x-line-signature: abcd1234...
body:
{ "events": [...] }

這裡的 body 還是 raw body


🔥 raw body 的重點來了(NestJS 的處理順序)

🟦 步驟 5:NestJS 預設會先把 raw body「解析成物件」

NestJS(底層是 Express/Fastify)會:

  1. 收到 raw body(byte stream)
  2. 用 JSON.parse 解析成 object
  3. 放進 req.body
  4. 丟掉原本 raw body(預設會丟掉)

所以如果你這樣寫:

1
2
3
4
@Post('webhook')
handleWebhook(@Body() body) {
console.log(body);
}

你拿到的是:

1
已經解析成物件的 body,不是 raw body

⚠️ 這時你的 raw body 已經被消耗掉,不能驗證 signature。


🟥 所以 LINE 說:「一定要先取 raw body」

你要在 NestJS 解析 JSON 之前取得 raw body。


🟩 步驟 6:NestJS 如何保留 raw body?(需要 middleware)

你要告訴 Express:

1
2
請不要吃掉 raw body
請在 request.rawBody 存一份 bytes 給我

例如:

1
2
3
4
5
6
7
app.use(
express.json({
verify: (req: any, res, buf) => {
req.rawBody = buf; // ★ 把 raw body 保存下來
},
}),
);

這樣:

1
2
req.rawBody 就是 LINE 計算簽名的原始 bytes
req.body 是 NestJS 解析後的 JSON 物件

🟦 步驟 7:你的 webhook handler 檢查 signature

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Post('webhook')
handleWebhook(
@Headers('x-line-signature') signature: string,
@Req() req: any
) {
const raw = req.rawBody; // raw body bytes
const secret = process.env.LINE_CHANNEL_SECRET;

const hash = createHmac('sha256', secret)
.update(raw)
.digest('base64');

if (hash !== signature) {
return 'Invalid signature';
}

return 'OK';
}

🎯 你的原始問題回答:raw body 在哪邊產生?

✔ raw body 是 LINE 伺服器產生的,不是 NestJS 產生的

它是 LINE server 將 webhook payload 轉成 bytes 後送給你。

✔ NestJS 預設會吃掉 raw body

所以你需要 middleware 把 raw body 留起來。

✔ 使用者在 LINE 打字上傳,只是「觸發」LINE server 產生 raw body

使用者 → LINE server(建立 JSON → raw body → HMAC → 傳給你)


🟦 用最白話講一次:

  • 使用者傳訊息 → LINE server 產生 JSON
  • LINE server 把 JSON 轉成 bytes(這就是 raw body)
  • LINE 用 raw body 計算 HMAC-SHA256(signature)
  • LINE 把 raw body + signature 一起送到你的 webhook
  • NestJS 會吃掉 raw body(除非你先留)
  • 你用 raw body 重算 HMAC 比對 signature → 才能驗證訊息真實性

如果你願意,我可以馬上幫你:

  1. 幫你寫好 NestJS 的 raw body middleware
  2. 幫你寫好完整的 webhook controller
  3. 幫你寫好 signature 驗證程式碼
  4. 幫你用 Mermaid 畫出整個流程圖

你要哪一個?