National Central University
Uedu Main Site
Explore Uedu
Student Console
Register as Member/Login
Research Informed Consent Center
Survey Center
Educational Big Data Challenge
Claim or create a course
Try an AI Scenario Task
Support & Messages
Uptime Data

UeduGPTs

--

Jupyters

3

Local AI

--

Uedu Code

--

中央大學 AQI 72 24°C PM2.5 13
AI Reply Desktop Notifications

Show a desktop notification when the AI TA finishes replying

Chat Message Notifications

Notify me when classmates post messages in the forum

Sound notification

Play an alert sound whenever there is a new notification

Unit 03

Webhook 與簽章驗證

為什麼要驗簽、HMAC 怎麼算、要快點回 200、重送事件
Interaction實作 30 minute(s)

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, 請求內容 ) )
  1. LINE 平台算出簽章,放在 X-Line-Signature 標頭。
  2. 你用自己的 Channel secret 和收到的內容再算一次。
  3. 兩邊相同才處理;不同就回錯誤,不處理事件。

這樣做成立的理由有兩個:沒有 secret 就算不出正確的簽章;內容只要改一個字,簽章就完全不同。

HMAC 只確認來源與內容沒被改過,不是加密,內容本身沒有被隱藏(傳輸的保護靠 HTTPS)。

比對時用 hmac.compare_digest,不用 ==。== 比到第一個不同的位置就停,花的時間可能透露資訊;compare_digest 就是為了降低這種時序分析的風險。

逐步看:自己驗證簽章

這裡不用 SDK,只用 Python 內建的 hmac、hashlib、base64。程式最下面送出三個請求:一個真的、一個亂填簽章、一個內容被改過。看 is_from_line 每次算出什麼。

互動練習:補上簽章驗證

這個機器人把收到的文字都記進資料庫,而且還沒有驗證簽章。面板上方的「假冒 LINE 送一個事件」會送出一個簽章是假的請求。

模擬環境為了讓你看到完整流程,假事件也帶著可用的 reply token,所以手機上看得到回覆。真實的攻擊者拿不到有效的 reply token,回覆會失敗;但寫進資料庫、呼叫外部服務這些事,在回覆之前就已經做完了。

任務請照順序做,第 3 個任務會遇到一個常見的錯誤。

驗簽要用原始內容

剛才第 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 處理過了沒」。

互動練習:重送事件不要重複簽到

這是讀書會的簽到機器人,改用 SDK 處理簽章。面板上方的「重送上一個事件」把最後一則訊息原封不動再送一次。

程式每次重新啟動都會清空資料表,方便你重新測試。

處理過的編號存在哪裡

練習裡用一個集合或查資料表都可以。正式環境要注意:

  • 記憶體裡的集合在程式重新啟動後就空了,多個程序之間也不共用,所以編號要存進資料庫
  • 把 webhookEventId 設成資料表的唯一索引(UNIQUE),兩個請求同時進來也只有一個寫得進去
  • 這張表不用永久保存,定期清掉舊資料即可

資料庫的用法第 9 課會細講,語法可以看 SQL 軌。

動手做:對官方文件的範例驗證一次

完成 verify(channel_secret, body, signature):算出 HMAC-SHA256、轉成 Base64,用 hmac.compare_digest 和 signature 比對,回傳 True 或 False。

測試資料直接取自 官方文件 公開的範例值。你的程式算出來如果和官方相同,就真的寫對了。

你的輸出
預期輸出

想一想

1.

為什麼不能只讓 LINE 平台的 IP 連到你的 webhook?

2.

下面這段程式會印出什麼?

3.

有人看到一次 webhook 的完整內容與簽章,他想改掉內容再送給你。為什麼做不到?

4.

重送的事件和原本的事件,差別在哪裡?

5.

要避免同一個事件被處理兩次,應該用什麼判斷?

6.

程式寫 event = data['events'][0]。在 Developers Console 按 Verify 時會怎樣?

第 1 次作答

重點整理

  • Webhook 網址是公開的,處理事件前一定要驗證簽章
  • 簽章=Base64(HMAC-SHA256(Channel secret, 原始內容)),比對用 hmac.compare_digest
  • 驗簽要用原始內容,不可以先解析再重組。SDK 的 handler.handle 會幫你算,但原始內容要由你交給它
  • events 可能有多個,也可能是空的,用 for 走訪,空的要回 200
  • callback 只放驗證和交辦,費時的工作放到背景(官方建議)
  • 同一個事件可能來不只一次,用 webhookEventId 去重,不要用 isRedelivery

下一課看除了文字之外還有哪些事件,以及怎麼知道是誰傳的。

這一課完成了!
Next lesson:事件與使用者
你目前沒有登入,學習進度只存在這個瀏覽器。 Login 後,進度會跟著帳號,換電腦也看得到。