在昨天的文章中,我們針對大語言模型進行了提示詞加固(System Prompt Hardening),成功抵禦了惡意的 Prompt Injection 與指令越獄企圖。
然而,除了在應用層防範「使用者說了什麼」,在通訊傳輸層還有一個非常嚴峻的資安風險:Webhook 偽造請求攻擊(Spoofing Attack)。
因為我們公開在公網上的 Webhook 接收網址(無論是 ngrok 還是雲端上的 n8n / FastAPI)本質上就是一隻公開的 HTTP POST 端點。如果今天有攻擊者拿到了這組網址,自行寫一個腳本模擬 LINE 的 JSON 格式發送假事件,系統若不加查驗,就會盲目地調用後端運算與 LLM 大腦,甚至造成嚴重的資料污染與額度耗損!
為了確保收到的每一筆 Webhook 封包「百分之百來自 LINE 官方伺服器」,今天 Day 23 我們將深入探討並實作 LINE 規範的 HMAC-SHA256 數位簽章驗證機制(X-Line-Signature)!
LINE 官方在每次發送 Webhook 給我們時,都會隨附一組數位簽章放在 HTTP Request Header 的 x-line-signature 欄位中。
這個簽章的生成機制如下:
接收端(我們的伺服器)在收到請求時,必須使用相同的 Channel Secret 與收到的 Raw Body 重新計算一次 HMAC-SHA256,並將計算結果與 Header 裡的 x-line-signature 比對:
401 Unauthorized 或 403 Forbidden!在微服務層實作簽章驗證,能提供最底層且嚴格的保護。
請確認 .env 中已加入 LINE Channel Secret:
LINE_CHANNEL_SECRET=your_32_character_channel_secret_here
建立 app/core/line_auth.py,使用 Python 內建的 hmac 與 hashlib 函式庫進行比對:
import hmac
import hashlib
import base64
import os
from fastapi import Request, HTTPException, status
from dotenv import load_dotenv
load_dotenv()
LINE_CHANNEL_SECRET = os.getenv("LINE_CHANNEL_SECRET", "")
async def verify_line_signature(request: Request):
"""
FastAPI 依賴注入函式:校驗 LINE Webhook 的 x-line-signature 簽章
"""
if not LINE_CHANNEL_SECRET:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="LINE_CHANNEL_SECRET 未在環境變數中設定"
)
# 1. 取得 Header 中的簽章
signature = request.headers.get("x-line-signature")
if not signature:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="缺少 x-line-signature 標頭,拒絕存取"
)
# 2. 取得原始 Request Body(二進位資料)
# ⚠️ 注意:必須使用原始 raw bytes,任何空格、換行或重排後的 JSON 都會使雜湊值完全不同!
body_bytes = await request.body()
# 3. 使用 HMAC-SHA256 重新計算雜湊
hash_obj = hmac.new(
key=LINE_CHANNEL_SECRET.encode("utf-8"),
msg=body_bytes,
digestmod=hashlib.sha256
)
# 4. 轉為 Base64 字串
calculated_signature = base64.b64encode(hash_obj.digest()).decode("utf-8")
# 5. 安全比對(使用 compare_digest 防止時序攻擊 Timing Attack)
if not hmac.compare_digest(signature, calculated_signature):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="數位簽章驗證失敗!該請求非來自 LINE 官方伺服器"
)
return True
在 FastAPI 中若有直接接收 LINE 事件的端點,只要掛上 Depends(verify_line_signature) 即可享有全自動攔截防護:
from fastapi import APIRouter, Depends, Request
from app.core.line_auth import verify_line_signature
router = APIRouter(prefix="/webhook", tags=["Webhook"])
@router.post("/line", dependencies=[Depends(verify_line_signature)])
async def line_webhook_endpoint(request: Request):
# 走到這裡代表 100% 通過簽章驗證
return {"status": "verified"}
如果你的架構是由 n8n 直接擔任第一線 Webhook 接收端,我們可以在 Webhook 節點後方緊接著放一個 Code (JavaScript) 節點進行簽章校驗!
1.打開 n8n 的 LINE Bot Core Workflow。
2.點開 Webhook 節點。
3.確保 Include In Output 中包含了 Headers(或勾選保留原始請求資訊)。
在 Webhook 節點與 Extract Event 節點之間插入一個 Code 節點:
Language:JavaScript
程式碼內容:
const crypto = require('crypto');
// 取得傳入的 header 簽章與請求內容
const headers = $input.first().json.headers;
const body = $input.first().json.body;
const lineSignature = headers['x-line-signature'] || headers['X-Line-Signature'];
const channelSecret = $env.LINE_CHANNEL_SECRET || '你的_CHANNEL_SECRET';
// 確保有簽章標頭
if (!lineSignature) {
throw new Error('拒絕存取:缺少 x-line-signature 標頭');
}
// 將 body 轉成字串進行 HMAC 雜湊運算
const bodyString = typeof body === 'string' ? body : JSON.stringify(body);
const calculatedSignature = crypto
.createHmac('SHA256', channelSecret)
.update(bodyString)
.digest('base64');
// 比對簽章
if (lineSignature !== calculatedSignature) {
throw new Error('安全性警報:數位簽章驗證不相符,判定為偽造請求!');
}
// 驗證通過,放行資料到下一個節點
return $input.first().json;
測試 1:攻擊者偽造請求(模擬惡意攻擊)
打開 PowerShell 或 Postman,直接對 Webhook 網址發送 POST 請求,但不帶 x-line-signature 標頭:
curl.exe -X POST "http://localhost:8000/webhook/line" `
-H "Content-Type: application/json" `
-d '{"events":[{"type":"message","message":{"type":"text","text":"偽造的免費查詢"}}]}'
驗收結果:
伺服器立即回傳:
{
"detail": "缺少 x-line-signature 標頭,拒絕存取"
}
HTTP 狀態碼為 401 Unauthorized,成功在第一時間擋下幽靈封包!
測試 2:簽章偽造或中間人篡改內容
發送請求時,隨意填寫一串錯誤的假簽章:
curl.exe -X POST "http://localhost:8000/webhook/line" `
-H "Content-Type: application/json" `
-H "x-line-signature: fake_invalid_signature_12345" `
-d '{"events":[]}'
驗收結果:
伺服器比對雜湊不一致,回傳:
{
"detail": "數位簽章驗證失敗!該請求非來自 LINE 官方伺服器"
}
HTTP 狀態碼為 403 Forbidden,請求完全無法進入後續的資料庫或業務流程!
測試 3:LINE 官方伺服器發送真實事件
1.拿起手機,在 LINE 聊天室中發送真實訊息。
2.檢查伺服器或 n8n 終端機日誌:
伺服器成功計算出與 LINE 伺服器完全相同的 HMAC-SHA256 Base64 字串。
請求順暢放行並完成推薦流程。
今日結語與明日預告
今天我們為整套通訊鏈路補齊了最關鍵的傳輸驗證拼圖:
深入拆解了 HMAC-SHA256 數位簽章與 Base64 的加解密機制。
在 FastAPI 透過非同步依賴注入,實現對 x-line-signature 的安全校驗。
徹底杜絕了來自公網的偽造 Webhook 呼叫,守護微服務的通訊主權!
至此,我們已經有了限流(Day 21)、提示詞防護(Day 22)以及數位簽章驗證(Day 23)三重堅實的防禦。
但隨著造訪人次越來越多,重複查詢同一個熱門地點(例如「西門町拉麵」或「板橋甜點」)會不斷消耗 Google Maps Places API。
明天 Day 24,我們將進入效能最佳化領域:引入 Redis / 快取機制(Caching),大幅降低端點延遲並顯著縮減 API 帳單開銷! 敬請期待!