~/blog/laravel-webhook-security-validation-guide.md
網站安全與防護 · 2025 / 09 / 15 · 3 views

Laravel Webhook 沒驗簽名,等於把 API 大門敞開:安全設計與驗證實戰

Eric — 浪花科技創辦人 / AI 架構師
Eric
浪花科技創辦人 · AI 架構師
Laravel Webhook 沒驗簽名,等於把 API 大門敞開:安全設計與驗證實戰
目錄 table-of-contents.md

沒有簽名驗證的 Webhook 端點,等於把 API 大門整扇敞開——任何人都能偽造請求,假裝自己是合法的來源系統。偏偏 Webhook 又是讓 WordPress 與 Laravel 優雅協同工作的關鍵橋樑,不能不用、更不能裸用。這篇從安全設計談到簽名驗證實戰,把這座橋的安檢一次做滿。

想像一下,你的 WooCommerce 網站每當有新訂單,就需要即時通知你的 Laravel ERP 系統更新庫存。傳統作法可能是讓 ERP 系統每隔幾分鐘就來「問」一次 WordPress:「欸,有新訂單嗎?」這就是所謂的輪詢 (Polling)。但這方法又笨又沒效率,就像個沒安全感的男友,每五分鐘打一次電話,超煩人,而且絕大多數時候都是白問一場,浪費資源。

Webhook 就不一樣了,它是「事件驅動」的。當新訂單這個「事件」發生時,WordPress 主動「推」一個通知給 Laravel 說:「嘿!新訂單來了,這是資料!」。這就是 Push vs. Pull 的概念,效率天差地遠。但,天下沒有白吃的午餐。當你打開一扇方便的大門,也可能引狼入室。今天,我就要來跟大家聊聊,如何正確地進行 Laravel Webhook 設計與驗證,確保你的自動化橋樑不只方便,更要固若金湯。

Webhook 的 A B C:從基礎概念到架構藍圖

在我們深入程式碼的細節前,先來打好基礎。簡單來說,Webhook 就是一個由事件觸發的 HTTP 回呼 (Callback)。當某個系統(發送方,例如 Stripe、GitHub、或你的 WordPress 網站)發生了特定事件,它會向你預先設定好的一個 URL(接收方,也就是我們的 Laravel 應用程式)發送一個 HTTP POST 請求,請求的 Body 中通常會包含事件相關的資料(通常是 JSON 格式)。

第一步:規劃你的 Webhook 接收路由 (Route)

萬事起頭難,但 Laravel 讓這件事變得很簡單。首先,我們得在 Laravel 裡開一個專門接收 Webhook 通知的大門,也就是一個路由。我的習慣是會把所有來自第三方服務的 Webhook 路由集中管理,並加上一個明確的前綴,例如 /api/webhooks/

打開你的 routes/api.php 檔案,加入類似這樣的路由:


<?php

use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Webhooks\StripeWebhookController;

// ... 其他路由

Route::post('/webhooks/stripe', [StripeWebhookController::class, 'handle']);

這裡有幾個小囉嗦要提醒一下:

  • 用 POST 方法:Webhook 幾乎都是 POST 請求,因為它們需要傳遞資料。
  • 放在 api.php:Webhook 本質上就是一種 API,放在這裡可以利用 Laravel API 路由的特性,例如它預設是無狀態的 (stateless)。
  • 明確的命名/webhooks/stripe 這樣的 URL 一看就知道它是幹嘛的,方便日後維護。千萬別用 /webhook-handler-1 這種鬼東西,未來的你會回來掐死現在的你。

第二步:建立專屬的控制器 (Controller)

路由定義好了,接著就是處理請求的邏輯。我們會建立一個專門的 Controller 來處理來自 Stripe 的 Webhook。


<?php

namespace App\Http\Controllers\Webhooks;

use Illuminate\Http\Request;
use App\Http\Controllers\Controller;

class StripeWebhookController extends Controller
{
    public function handle(Request $request)
    {
        // 接收到的 payload
        $payload = $request->all();

        // TODO: 在這裡處理 Webhook 邏輯
        // 例如:根據事件類型(event type)做不同的事
        switch ($payload['type']) {
            case 'checkout.session.completed':
                // 處理付款成功
                break;
            case 'customer.subscription.deleted':
                // 處理訂閱取消
                break;
            // ... 其他事件
        }

        // 回應 200 OK 告訴發送方「我收到了,沒問題」
        return response()->json(['status' => 'success'], 200);
    }
}

看到這裡,你可能會覺得:「欸,不就這樣?很簡單嘛!」
錯!大錯特錯! 如果你就這樣上線,等於是把家裡大門敞開,門上還貼著「歡迎光臨」。任何知道你這個 URL 的人,都可以偽造一個請求,隨意觸發你系統裡的任何邏輯。這就是我們接下來要談的重中之重:驗證。

滴水不漏:Laravel Webhook 的終極安全驗證

Webhook 的安全性是最多人忽略,也最致命的一環。你必須假設所有打進來的請求都是惡意的,直到你能證明它的清白。這就是「零信任」原則。而驗證 Webhook 的黃金標準,就是簽名驗證 (Signature Validation)

簽名驗證是如何運作的?

概念其實不複雜:

  1. 你在第三方服務(如 Stripe)的後台設定 Webhook 時,它會給你一組「簽名密鑰 (Signing Secret)」。這組密鑰只有你和它知道。
  2. 當 Stripe 要發送 Webhook 給你時,它會用這個密鑰,加上請求的時間戳 (timestamp) 和請求的內容 (payload),透過一個加密演算法(通常是 HMAC-SHA256)產生一個獨一無二的「簽名 (Signature)」。
  3. 這個簽名會被放在請求的 Header 中(例如 Stripe-Signature)一起發送過來。
  4. 你的 Laravel 應用程式收到請求後,用完全相同的方式(使用你預存的密鑰、收到的時間戳和 payload)在你的伺服器上重新計算一次簽名。
  5. 最後,比對你算出來的簽名,跟 Header 裡收到的簽名是否一模一樣。如果一樣,就代表這個請求確實是 Stripe 發的,而且內容在傳輸過程中沒有被竄改。如果不一樣,直接回傳 403 Forbidden,連理都不要理它。

實戰:用 Middleware 打造可重用的驗證層

這種驗證邏輯,我們不應該寫在 Controller 裡,因為它會跟業務邏輯混在一起,而且如果有很多個 Webhook,你就得複製貼上很多次。最好的做法是建立一個 Middleware。

首先,用 Artisan 建立一個 Middleware:

php artisan make:middleware VerifyStripeWebhook

接著,打開 app/Http/Middleware/VerifyStripeWebhook.php,把我們的驗證邏輯放進去:


<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class VerifyStripeWebhook
{
    public function handle(Request $request, Closure $next): Response
    {
        // 從 config 或 .env 取得你的 Webhook Secret
        $secret = config('services.stripe.webhook_secret');

        // 從 header 取得 Stripe 傳來的簽名
        $signature = $request->header('Stripe-Signature');

        // 如果 secret 或 signature 不存在,直接拒絕
        if (!$secret || !$signature) {
            abort(403, 'Webhook secret or signature not found.');
        }

        try {
            // 使用 Stripe 官方的 SDK 來驗證是最保險的作法
            // 這段程式碼需要 `stripe/stripe-php` 套件
            \Stripe\Webhook::constructEvent(
                $request->getContent(), $signature, $secret
            );
        } catch (\UnexpectedValueException $e) {
            // Payload 無效 (e.g., malformed JSON)
            abort(400, 'Invalid payload.');
        } catch (\Stripe\Exception\SignatureVerificationException $e) {
            // 簽名無效
            abort(403, 'Invalid signature.');
        }

        // 驗證通過,繼續處理請求
        return $next($request);
    }
}

寫好 Middleware 後,要去 app/Http/Kernel.php 註冊它:


protected $routeMiddleware = [
    // ... 其他 middleware
    'verify.stripe' => \App\Http\Middleware\VerifyStripeWebhook::class,
];

最後,在你的路由上套用這個 Middleware:


Route::post('/webhooks/stripe', [StripeWebhookController::class, 'handle'])
     ->middleware('verify.stripe');

大功告成!現在所有要進入 StripeWebhookController 的請求,都必須先通過 VerifyStripeWebhook 這個保全的嚴格檢查。你的 Controller 就可以專心處理乾淨、可信的業務邏輯,是不是清爽多了?

從優秀到卓越:Webhook 的進階處理技巧

做完驗證,你的 Webhook 系統已經達到 80 分了。但身為一個追求卓越的工程師,我們還能做得更好。

非同步處理:別讓發送方等太久

Webhook 的處理邏輯可能很複雜,例如要更新資料庫、發送 Email、呼叫其他 API 等等。如果這些事情都同步執行,可能會花上好幾秒。但 Webhook 發送方通常有超時限制(例如 3-5 秒),如果你的程式沒在時間內回覆 200 OK,它可能會認為你沒收到,然後進行重試,造成重複處理的問題。

最佳解法是:非同步處理。

在你的 Controller 裡,收到請求並驗證 payload 的基本格式後,馬上把這個任務丟到佇列 (Queue) 裡,然後立刻回傳 200 OK。這樣發送方就心滿意足地離開了,而真正的繁重工作由背景的 Queue Worker 去慢慢處理。


// In StripeWebhookController.php
use App\Jobs\ProcessStripeWebhook;

public function handle(Request $request)
{
    // 把整個 payload 丟到 Queue Job 裡
    ProcessStripeWebhook::dispatch($request->all());

    // 馬上回覆,讓 Stripe 安心
    return response()->json(['status' => 'queued'], 200);
}

這樣做不僅能避免超時,還能提高系統的可靠性。就算你的資料庫暫時連不上,任務會留在佇列裡,等恢復後再試。想更深入了解 Laravel 的佇列與背景任務,可以參考我之前寫的這篇文章

日誌與監控:留下破案的線索

最後一個小囉嗦,但絕對重要:記錄日誌 (Logging)。當事情出錯時,日誌是你唯一的線索。你應該記錄每一筆收到的 Webhook 請求(包含 Header 和 Body),以及你的處理結果(成功、失敗、失敗原因)。當客戶跟你說「我付款了但沒收到商品」時,你才能從日誌中迅速找出問題,而不是兩手一攤說不知道。

Laravel 內建的 Log 功能非常好用,善用它,未來的你會感謝現在的你。

總結

Webhook 是串連不同系統、實現自動化的強大工具。一個設計良好的 Webhook 系統,可以讓你的應用程式生態系活起來。今天我們從 Laravel Webhook 設計與驗證 的基礎出發,涵蓋了從路由規劃、安全驗證到非同步處理的完整流程。

記住幾個關鍵心法:

  • 專屬路由:為每個 Webhook 來源建立清晰的路由和控制器。
  • 驗證至上:絕對、絕對、絕對要實作簽名驗證,不要相信任何未經驗證的請求。
  • 善用 Middleware:將驗證邏輯從業務邏輯中抽離,保持程式碼乾淨可重用。
  • 非同步處理:用佇列處理耗時任務,提高系統的回應速度和穩定性。
  • 詳實記錄:日誌是你的救生索,一定要做好。

掌握了這些原則,你就能自信地打造出既高效又安全的 Webhook 系統,無論是串接金流、CRM、還是各種 SaaS 服務,都能得心應手。


延伸閱讀


在數位轉型的路上,系統之間的串接與自動化是提升效率的關鍵。如果你們的企業正在尋求更穩定、更安全的系統整合方案,或是對 WordPress 與 Laravel 的混合應用有任何疑問,浪花科技的團隊擁有豐富的實戰經驗。歡迎點擊這裡,填寫表單聯繫我們,讓我們一起打造更聰明的數位工作流程!

// FAQ

常見問題

Webhook 與 API 輪詢(Polling)有什麼差別?
API 輪詢是接收方每隔一段時間主動向發送方詢問「是否有新資料」,效率低且浪費資源。Webhook 則是事件驅動的推送模式,當事件發生時發送方主動把資料推送到接收方指定的 URL,屬於 Push 模式,即時且高效。
在 Laravel 中接收 Webhook 的路由應該怎麼設計?
Webhook 路由應使用 POST 方法並定義在 routes/api.php 中,以利用 API 路由無狀態的特性。建議集中管理並加上明確前綴,例如 /api/webhooks/stripe,讓 URL 一看就知道用途,方便日後維護。
Webhook 的簽名驗證(Signature Validation)是如何運作的?
第三方服務後台會提供一組只有雙方知道的簽名密鑰,發送 Webhook 時會用該密鑰、時間戳與內容透過加密演算法(通常是 HMAC-SHA256)產生簽名,放在請求 Header(如 Stripe-Signature)中。接收端用相同方式重新計算簽名並比對,一致才代表請求合法且未被竄改,不一致應回傳 403 Forbidden。
Webhook 的驗證邏輯應該寫在哪裡比較好?
建議用 Middleware 而非寫在 Controller 裡,避免驗證邏輯與業務邏輯混雜,也能在多個 Webhook 之間重複使用。可用 php artisan make:middleware 建立,在其中完成簽名驗證後再放行請求。
驗證 Stripe Webhook 時應該用什麼方式最保險?
使用 Stripe 官方 SDK(stripe/stripe-php 套件)的 Webhook::constructEvent 方法來驗證最為保險,它會同時檢查 payload 與簽名。若 payload 格式無效應回傳 400,若簽名驗證失敗(SignatureVerificationException)應回傳 403。
~/roamer-tech/newsletter // FREE
// newsletter

訂閱免費電子報

把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。

$
// final.exec()

準備好讓你的網站開始為你工作了嗎?