Learning Objectives
- 說明 webhook 請求裡每個主要欄位的用途
- 說明為什麼要驗證簽章,以及少了它會發生什麼事
- 用 hmac 與 base64 手寫一次簽章驗證,並說明為什麼要用原始內容
- 說明回 2xx、非同步處理與 Verify 按鈕的關係
- 用 webhookEventId 讓重複的事件不會被處理兩次
任何人都能 POST 你的網址
Webhook 網址必須公開,LINE 平台才連得到;這也代表任何人只要知道網址,都能送一個長得和 LINE 平台一模一樣的請求過來。如果機器人收到事件就照做,別人就能假冒某位同學簽到、塞假訊息進你的資料庫,或讓機器人對外發公告。
官方的 Verify webhook signature 也寫明 LINE 平台的 IP 位址不公開,要靠簽章確認來源,不是靠擋 IP。這一課先把請求內容拆開來看,再親手算一次簽章。
Webhook 的內容
LINE 平台送來的內容是一個 JSON 物件,最外層有兩個欄位:
| 欄位 | 意思 |
|---|---|
destination |
應該收到這些事件的機器人,它的 userId |
events |
事件串列。可能有好幾個,也可能是不同人的;測試連線時是空的 |
events 裡每個事件的常見欄位:
| 欄位 | 意思 |
|---|---|
type |
事件種類,例如 message、follow |
timestamp |
事件發生的時間(毫秒)。重送時也不變,是發生的時間,不是送達的時間 |
source |
事件來源,一對一聊天時是 {"type": "user", "userId": "..."} |
replyToken |
回覆這個事件時要交回去的憑證。有些事件沒有,例如封鎖 |
mode |
active 是正常;standby 時沒有 reply token,機器人不應該傳訊息 |
webhookEventId |
這個事件的唯一編號 |
deliveryContext.isRedelivery |
這是不是重送的事件 |
message |
訊息事件才有,內容依種類不同 |
真實的 userId 是 U 加上 32 個十六進位字元,這個課程用的是縮短的假值。
逐步看:一次 webhook 的欄位
line_sim 只存在於這個課程,這裡用它產生和 LINE 平台相同格式的內容。對照輸出,一個一個認識上面表格的欄位。
簽章怎麼算
LINE 平台和你各有一份 Channel secret,別人沒有。每次送 webhook 時:
簽章 = Base64( HMAC-SHA256( Channel secret, 請求內容 ) )
- LINE 平台算出簽章,放在
X-Line-Signature標頭。 - 你用自己的 Channel secret 和收到的內容再算一次。
- 兩邊相同才處理;不同就回錯誤,不處理事件。
這樣做成立的理由有兩個:沒有 secret 就算不出正確的簽章;內容只要改一個字,簽章就完全不同。
HMAC 只確認來源與內容沒被改過,不是加密,內容本身沒有被隱藏(傳輸的保護靠 HTTPS)。
比對時用 hmac.compare_digest,不用 ==。== 比到第一個不同的位置就停,花的時間可能透露資訊;compare_digest 就是為了降低這種時序分析的風險。
逐步看:自己驗證簽章
這裡不用 SDK,只用 Python 內建的 hmac、hashlib、base64。程式最下面送出三個請求:一個真的、一個亂填簽章、一個內容被改過。看 is_from_line 每次算出什麼。
驗簽要用原始內容
剛才第 3 個任務,程式先把 JSON 解析成字典,再用 json.dumps 轉回字串。資料的意思沒變,字串卻不同了:
>>> body = '{"a":1,"b":[]}'
>>> json.dumps(json.loads(body))
'{"a": 1, "b": []}'
多了幾個空白,簽章就對不上。簽章是對位元組算的,不管 JSON 的意思。官方文件也列了常見的驗簽失敗原因:
| 原因 | 說明 |
|---|---|
| 驗證前解析或重新排版 JSON | 就是剛才的情況 |
| 中間的 proxy、負載平衡器改了內容或標頭 | 在到達你的程式之前就變了 |
| 用了 HMAC-SHA256 以外的演算法 | 例如 HMAC-SHA1 |
| 用了別的 Channel 的 secret | 每個 Channel 的 secret 不同 |
| secret 被別人重新核發 | 舊的立刻失效,程式要換成新的 |
| 跳脫字元被解讀、編碼不是 UTF-8 | 內容裡的 \n 或表情符號會被改變 |
實際寫機器人時不用自己算。SDK 的 handler.handle(body, signature) 第一步就是驗證,內部一樣用 compare_digest,失敗就丟 InvalidSignatureError。你要做的是把原始內容交給它,並接住例外回 400,這就是第 2 課 callback 那幾行的用意。
另外,官方寫明 LINE 平台可能送 events 是空陣列的請求來確認連線,這時要回 200。所以程式要用 for 走訪事件,不要寫 data['events'][0]:那樣在空陣列時會丟 IndexError,回 500。
回應與費時的工作
官方的規定是:收到 webhook 後回 200。不是 2xx 的回應會被當成失敗,長期失敗時 LINE 平台還可能暫停送 webhook 給你。
官方文件建議非同步處理事件,理由是避免後面的請求要等前面的處理完。另外,Webhook 錯誤統計的說明寫明,伺服器 2 秒內沒有回應會被記為 request_timeout(2026-11-03 查證,請以官方頁面為準)。所以 callback 裡只放驗證和交辦,費時的工作(呼叫語言模型、查外部服務)放到背景。
要在背景做事,得把「驗證」和「處理」分開。handler.handle 兩件事一起做,所以這裡改用 SDK 的 WebhookParser:它只驗證並解析,把事件交給你。
瀏覽器沒有執行緒,所以這個課程裡 Thread.start() 會原地執行完才往下走,time.sleep 也只是把模擬的時鐘往前撥,不會真的等。在真實環境裡,callback 會先回 200,背景工作才繼續做。
逐步看:驗證留在請求裡,工作放到背景
第二句話「慢慢查」要花 70 秒,比較兩句話的結果。背景處理解決了「回應太慢」,但解決不了 reply token 會過期。
同一個事件可能來兩次
Developers Console 可以開啟 Webhook redelivery(預設關閉)。開啟後,你的伺服器沒回 2xx 時,LINE 平台會在一段時間內重送,次數與間隔不公開,而且官方說明不保證一定送達。
重送的內容和原本幾乎相同,只有 deliveryContext.isRedelivery 變成 true,webhookEventId 與 replyToken 都不變。如果第一次已經用掉 reply token,重送時就不能再用。
官方也提醒兩件事:
- 就算沒有開重送,同一個事件也可能因為網路路由等原因送達不只一次,要用
webhookEventId偵測重複 - 重送會讓事件的抵達順序和發生順序不同,需要時用
timestamp判斷
所以處理事件要做到冪等(idempotent):同一個事件處理兩次,結果和處理一次相同。
不要用 isRedelivery 判斷要不要處理。第一次如果在做事之前就失敗了,重送正是補救的機會,跳過就等於弄丟事件;反過來,非重送造成的重複,它的 isRedelivery 也可能是 false。判斷的依據是「這個 webhookEventId 處理過了沒」。
處理過的編號存在哪裡
練習裡用一個集合或查資料表都可以。正式環境要注意:
- 記憶體裡的集合在程式重新啟動後就空了,多個程序之間也不共用,所以編號要存進資料庫
- 把
webhookEventId設成資料表的唯一索引(UNIQUE),兩個請求同時進來也只有一個寫得進去 - 這張表不用永久保存,定期清掉舊資料即可
資料庫的用法第 9 課會細講,語法可以看 SQL 軌。
動手做:對官方文件的範例驗證一次
完成 verify(channel_secret, body, signature):算出 HMAC-SHA256、轉成 Base64,用 hmac.compare_digest 和 signature 比對,回傳 True 或 False。
測試資料直接取自 官方文件 公開的範例值。你的程式算出來如果和官方相同,就真的寫對了。
想一想
為什麼不能只讓 LINE 平台的 IP 連到你的 webhook?
下面這段程式會印出什麼?
有人看到一次 webhook 的完整內容與簽章,他想改掉內容再送給你。為什麼做不到?
重送的事件和原本的事件,差別在哪裡?
要避免同一個事件被處理兩次,應該用什麼判斷?
程式寫 event = data['events'][0]。在 Developers Console 按 Verify 時會怎樣?
重點整理
- Webhook 網址是公開的,處理事件前一定要驗證簽章
- 簽章=Base64(HMAC-SHA256(Channel secret, 原始內容)),比對用
hmac.compare_digest - 驗簽要用原始內容,不可以先解析再重組。SDK 的
handler.handle會幫你算,但原始內容要由你交給它 events可能有多個,也可能是空的,用for走訪,空的要回 200callback只放驗證和交辦,費時的工作放到背景(官方建議)- 同一個事件可能來不只一次,用
webhookEventId去重,不要用isRedelivery
下一課看除了文字之外還有哪些事件,以及怎麼知道是誰傳的。