Laravel Webhook 設計與驗證實戰:別讓駭客偽造請求炸穿你的資料庫

2026/01/26 | API 串接與自動化, Laravel技術分享, 全端與程式開發, 網站安全與防護

終結 Webhook 裸奔:Laravel 資安三道防線

Webhook 接收端常因缺乏簽章驗證而成為駭客的提款機!資深工程師揭露,僅依賴 HTTPS 遠遠不夠,你的系統隨時面臨偽造「付款成功」請求的災難性風險。本文提供硬核實戰指導,教你如何在 Laravel 專案中,部署三道關鍵防線:透過 HMAC 雜湊簽章,杜絕偽造請求(切記使用 hash_equals 防止時序攻擊);利用冪等性機制,處理重複通知;並採用 Queue 非同步處理,確保系統在高併發下仍能秒回 200 OK。別讓你的資料庫裸奔,立即升級你的 Webhook 安全架構,打造滴水不漏的資安堡壘!

需要專業協助?

聯絡浪花專案團隊 →

Laravel Webhook 設計與驗證實戰:別讓駭客偽造請求炸穿你的資料庫

嗨,我是 Eric,浪花科技的資深工程師。又到了我們聊聊 Code 的時間。

前幾天我在 Review 一位剛轉職過來的後端工程師的程式碼,看到他寫了一個用來接收第三方金流(Payment Gateway)通知的 Webhook API。功能看起來沒問題,邏輯也跑得通,但我眉頭一皺,發現案情並不單純。

「這隻 API 怎麼沒有驗證簽章(Signature Verification)?」我問。

他一臉無辜地看著我:「Eric 哥,文件上說只要開一個 POST 路由接收 JSON 就可以啦,而且我們有設 HTTPS,應該很安全吧?」

聽完這句話,我差點把手上的咖啡捏爆。HTTPS 只是保證傳輸過程加密,並不保證傳送資料的人是誰啊!這就像是你家裝了防盜門,但你把備用鑰匙放在門口地墊下一樣,隨便誰都能進來。

如果你的 Laravel Webhook 處於「裸奔」狀態,駭客只需要知道你的 API URL(這其實不難猜),就可以模擬金流商發送一個「付款成功」的請求,你的系統就會乖乖地幫他開通會員或出貨。這不是 Bug,這是災難。

今天這篇文章,我們不談太虛無縹緲的架構,直接來點硬核的。我會教你如何在 Laravel 10/11 中,設計一個安全、高併發且具備容錯能力的 Webhook 接收端。我們會用到 Middleware、HMAC 雜湊驗證以及 Queue(佇列)。

什麼是 Webhook?為什麼它這麼危險?

簡單來說,Webhook 就是一種「反向 API」。通常是我們去 Call 別人的 API 拿資料,而 Webhook 則是別人(例如 Stripe、LINE、GitHub)在特定事件發生時(例如付款成功、有人 Push Code),主動發送 HTTP POST 請求到我們伺服器上的特定 URL。

這是一個被動接收的過程。

正因為它是被動接收,最大的資安風險就在於:「你如何證明發送這個請求的人,真的是 Stripe 或 LINE,而不是隔壁老王?」

如果沒有驗證機制,任何人都可以用 Postman 對你的 /api/webhook/payment 發送假資料:

{
  "event": "payment.success",
  "amount": 10000,
  "user_id": 888
}

然後你的系統就傻傻地幫 user_id: 888 加值了一萬元。可怕吧?

第一道防線:設計驗證簽章 (Signature Verification)

幾乎所有正規的第三方服務(Stripe, PayPal, LINE, GitHub)在發送 Webhook 時,都會在 Header 裡帶上一個簽章(Signature)。這個簽章通常是使用 HMAC-SHA256 演算法生成的。

原理很簡單:

  1. 第三方服務手上有一個 Secret Key(你也有一份,存在 .env 裡)。
  2. 他們把要傳給你的 JSON Payload 和 Timestamp,用這個 Key 進行雜湊運算,產生一串亂碼(簽章)。
  3. 他們把 Payload 和這串簽章一起寄給你。
  4. 你收到後,用同樣的 Key 和收到的 Payload 再算一次。
  5. 如果你算出來的結果,跟他們傳過來的一模一樣,那就代表資料中途沒被竄改,且發送者擁有正確的 Key。

這就像是以前間諜電影裡的「對暗號」。

Laravel 實作範例

我不建議把驗證邏輯寫在 Controller 裡,這樣程式碼會很髒。最好的位置是在 Middleware(中介層)。這樣我們可以針對特定的 Route 套用驗證。

首先,建立一個 Middleware:

php artisan make:middleware VerifyWebhookSignature

接著,編輯 app/Http/Middleware/VerifyWebhookSignature.php。這裡我們模擬一個通用的 HMAC-SHA256 驗證邏輯(具體 Header 名稱要看對接的廠商,例如 GitHub 是 X-Hub-Signature-256,Stripe 是 Stripe-Signature)。

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
use Illuminate\Support\Facades\Log;

class VerifyWebhookSignature
{
    public function handle(Request $request, Closure $next): Response
    {
        // 1. 從 Header 取得簽章 (這裡假設廠商放在 X-Signature)
        $signature = $request->header('X-Signature');

        if (!$signature) {
            Log::warning('Webhook 缺少簽章');
            return response()->json(['message' => 'Missing Signature'], 403);
        }

        // 2. 取得我們的 Secret Key (記得放在 .env)
        $secret = config('services.webhook.secret');

        if (!$secret) {
            Log::error('系統尚未設定 Webhook Secret');
            return response()->json(['message' => 'Server Error'], 500);
        }

        // 3. 取得原始 Payload (Raw Body)
        // 注意:有些框架會自動解析 JSON,導致空格或換行改變,算出來的 Hash 就會錯。
        // 在 Laravel 務必使用 getContent() 拿原始字串。
        $payload = $request->getContent();

        // 4. 計算預期的簽章 (HMAC SHA256)
        // 注意:有些廠商的簽章前面會帶有 "sha256=" 字串,需要先處理。
        $expectedSignature = hash_hmac('sha256', $payload, $secret);

        // 5. 比對簽章 (使用 hash_equals 防止時序攻擊)
        if (!hash_equals($expectedSignature, $signature)) {
            Log::warning('Webhook 簽章驗證失敗', ['ip' => $request->ip()]);
            return response()->json(['message' => 'Invalid Signature'], 403);
        }

        return $next($request);
    }
}

這裡有一個工程師常犯的錯誤:絕對不要用 == 來比較字串。因為一般的字串比較在發現第一個不同字元時就會停止,這會讓駭客有機可乘,利用「時序攻擊(Timing Attack)」來慢慢猜出你的簽章。請愛用 PHP 內建的 hash_equals()

第二道防線:處理重複請求 (Idempotency)

網際網路是不穩定的。有時候對方送出了 Webhook,但因為網路延遲,他以為傳送失敗(Timeout),於是啟動了重試機制(Retry)。結果你的伺服器其實收到了,只是回應慢了一點。

這時候,你會收到兩次內容完全一樣的請求。如果你沒有處理「冪等性(Idempotency)」,你的使用者可能會被扣款兩次,或者發送兩次通知信。

解決方案:使用 Cache 記錄 Event ID

通常 Webhook Payload 裡會有一個唯一的 idevent_id。我們可以用 Redis 來記錄這個 ID 是否已經處理過。

// 在 Controller 或 Job 中

$eventId = $payload['id'];
$cacheKey = "processed_webhook_{$eventId}";

// 檢查是否處理過 (設定 24 小時過期即可)
if (Cache::has($cacheKey)) {
    Log::info("重複的 Webhook ID: {$eventId},直接略過");
    return response()->json(['message' => 'Already Processed']);
}

// ... 執行你的商業邏輯 ...

// 標記為已處理
Cache::put($cacheKey, true, now()->addHours(24));

第三道防線:非同步處理 (Queue)

這是我最常碎碎念新人的點。Webhook 的接收端必須「快進快出」

很多服務(如 LINE)對 Webhook 有嚴格的 Timeout 限制(例如 5 秒或 10 秒)。如果你在接收到 Webhook 後,還去 Call 另一個 API、寫入龐大的資料庫、甚至發 Email,很容易就會超時。

一旦超時,對方就會認為失敗,然後開始瘋狂重試(Retry),這會導致你的伺服器負載瞬間飆高,甚至形成 DDoS 攻擊的效果。

正確架構:

  1. Controller:驗證簽章 -> 丟進 Queue (Job) -> 立刻回傳 HTTP 200 OK
  2. Worker:在背景慢慢消化這些 Job,執行耗時的商業邏輯。

這樣的架構不僅能應付瞬間的高併發(例如雙 11 的訂單通知),還能確保即使你的資料庫暫時掛掉,Job 也會保留在 Queue 裡,稍後可以重試,不會掉單。

實戰配置範例

最後,記得在 routes/api.php 中套用我們剛剛寫的 Middleware:

use App\Http\Controllers\WebhookController;
use App\Http\Middleware\VerifyWebhookSignature;

Route::post('/webhook/payment', [WebhookController::class, 'handle'])
    ->middleware(VerifyWebhookSignature::class);

WebhookController 中:

public function handle(Request $request)
{
    // 這裡不需要再驗證簽章了,Middleware 做過了
    
    $payload = $request->all();
    
    // 丟給 Job 處理
    ProcessPaymentWebhook::dispatch($payload);
    
    // 秒回 200
    return response()->json(['status' => 'success']);
}

總結:安全是設計出來的,不是測出來的

Webhook 的串接看似簡單,但魔鬼都在細節裡。從簽章驗證(防止偽造)、冪等性檢查(防止重複執行),到使用 Queue 非同步處理(防止逾時與塞車),每一個環節都是資深工程師與新手的差距所在。

不要等到資料庫被寫入一堆假訂單,或者因為逾時導致服務被第三方封鎖時,才來後悔沒有把基礎建設打好。既然要寫 Code,我們就寫出那種即使在週五晚上部署,也能讓你安心睡好覺的 Code。

延伸閱讀

你的 Laravel 專案需要更嚴謹的資安架構與效能優化嗎?別讓技術債拖垮你的業務成長!
立即聯繫浪花科技,諮詢企業級解決方案

常見問題 (FAQ)

Q1: 為什麼要用 hash_equals 而不是 == 來比較簽章?

普通的字串比較運算子(== 或 ===)在比對過程中,一旦發現第一個不相符的字元就會立即停止比對並回傳 false。這種「不固定時間」的回應特徵,讓駭客可以透過測量回應時間的微小差異(Timing Attack),逐字猜出正確的簽章內容。使用 hash_equals 則能保證無論字串是否相符,比較所需的時間都是固定的,從而杜絕這種攻擊。

Q2: 本地開發環境 (Localhost) 收不到外部的 Webhook 怎麼辦?

這是因為你的 localhost 沒有公開的 IP 地址。推薦使用工具如 Ngrok、Expose 或 Cloudflare Tunnel,它們可以建立一個暫時的公開 URL,並將流量轉發到你本機的 Laravel 專案(例如 localhost:8000),方便進行測試與除錯。

Q3: 如果第三方服務沒有提供簽章驗證怎麼辦?

這很不安全,但如果無法避免,建議採取以下補救措施:1. 在 URL 中加入一個隨機且夠長的 Token(例如 /webhook/payment?token=your_secret_token)並驗證它。2. 設定 IP 白名單,只允許該服務公告的 IP 來源存取你的 API。3. 盡快建議該服務商升級他們的安全性。

 
立即諮詢,索取免費1年網站保固