Laravel 防線告急?手刻驗證 (Validation) 與中介層 (Middleware),打造駭客繞道的安全防線!
☰ 目錄 table-of-contents.md
Laravel 安全防線該怎麼做?
只用 'email' => 'required|email' 這類內建規則就覺得 API 夠安全,等於只在大門裝了個喇叭鎖。一條穩固的 Laravel 防線需要兩道把關:客製化驗證規則(Custom Validation Rule)負責檢查「資料內容是否符合商業邏輯」,客製化中介層(Custom Middleware)則在請求進入控制器前先攔下「沒資格的請求」。
說穿了,Validation 管的是「進來的資料對不對」,Middleware 管的是「這個請求該不該進來」。兩者分工清楚、各司其職,Controller 才能保持乾淨,同時把非法請求擋在外面。底下用優惠券驗證與 API 金鑰兩個實戰範例,帶你把這套防線從 0 到 1 建起來。
身為一個天天在 Code 海裡打滾的工程師,我看過太多 Laravel 新手把專案當作一個開放式樂園,大門敞開,誰都能進來玩。他們可能會說:「我有用 Laravel 內建的 validation 啊!'email' => 'required|email',很安全吧?」
每次聽到這個,我工程師的小囉嗦模式就會自動開啟。兄弟,這就像是你家大門裝了個喇叭鎖,就自以為固若金湯了。真正的戰場上,你需要的是護城河、是瞭望塔,是層層把關的鐵衛。在 Laravel 的世界裡,這對無敵的守衛組合,就是「客製化驗證(Custom Validation)」與「客製化中介層(Custom Middleware)」。
今天,就讓我帶你跳脫官方文件的基礎教學,深入這兩個 Laravel 核心功能的精髓,從 0 到 1 打造出真正企業級、讓駭客看到都想繞道的應用程式防線。
客製化驗證與 Middleware 差在哪?該用哪一個?
動手寫程式之前,先把職責分清楚,後面才不會把邏輯放錯地方。它們處在請求生命週期的不同階段:
- Middleware(中介層):站在請求生命週期的最前端,在請求還沒碰到控制器與驗證之前就先攔截。它關心的是「這個請求有沒有資格繼續往下走」——身份、金鑰、權限、來源。
- Validation(驗證):在請求進入控制器(或 Form Request)後,檢查使用者送進來的資料內容是否合規——格式對不對、商業規則符不符合。
一個簡單的判斷準則:如果你的檢查不依賴「使用者填了什麼欄位」,而是針對整個請求(例如 Header 裡的 API Key、登入者的角色),那就放 Middleware;如果你的檢查是針對某個欄位的值(例如優惠券代碼是否有效),那就放 Validation。下面我們分別實戰。
第一道防線:客製化驗證規則 — 不只驗證格式,更要驗證商業邏輯
Laravel 內建的驗證規則(required、email、max、min⋯)非常方便,處理常規的表單綽綽有餘。但當你的業務邏輯開始變得複雜時,它們就顯得捉襟見肘了。這時候,就是客製化驗證規則登場的最佳時機。
什麼時候該手刻驗證規則?
當你的驗證邏輯符合以下任一情境時,別懷疑,就是它了:
- 複雜的格式驗證:例如台灣的身分證字號、手機條碼、公司統一編號,這些都有固定的演算法,不是一個簡單的正規表達式就能搞定的。
- 需查詢資料庫的驗證:例如,驗證使用者輸入的優惠券代碼是否存在、是否已過期、是否已被使用。
- 跨欄位的相依性驗證:例如,當「運送方式」選擇「宅配」時,「運送地址」欄位才變成必填。
- 可重用的商業邏輯:如果某個驗證邏輯會在多個地方被使用(例如,A 表單和 B API 都需要驗證同一個東西),把它封裝成一個獨立的 Rule Object 是最乾淨的做法。
實戰演練:打造一個「優惠券驗證」規則
想像一下,我們正在開發一個電商網站,需要一個驗證 Coupon Code 的規則。這個規則需要檢查三件事:1. Code 存在於資料庫 2. 尚未過期 3. 未被使用。光用內建規則肯定做不到。來,打開你的終端機,跟著我一起做。
步驟一:建立 Rule 物件
Artisan 指令是我們最好的朋友,一行指令就能建立好我們的規則檔案結構:
php artisan make:rule IsValidCoupon
這會在 app/Rules 資料夾下建立一個 IsValidCoupon.php 檔案。
步驟二:撰寫驗證邏輯
打開 IsValidCoupon.php,你會看到兩個主要的方法:passes() 和 message()。這就是我們的主戰場。
passes($attribute, $value):這是驗證的核心。$value就是使用者傳入的值(我們的 coupon code)。這個方法必須回傳true(驗證通過)或false(驗證失敗)。message():當驗證失敗時,要回傳給使用者的錯誤訊息。
讓我們把商業邏輯寫進去:
<?php
namespace App\Rules;
use Illuminate\Contracts\Validation\Rule;
use App\Models\Coupon; // 假設你有一個 Coupon 的 Eloquent Model
use Carbon\Carbon;
class IsValidCoupon implements Rule
{
/**
* Determine if the validation rule passes.
*
* @param string $attribute
* @param mixed $value
* @return bool
*/
public function passes($attribute, $value)
{
// 1. 檢查優惠券是否存在
$coupon = Coupon::where('code', $value)->first();
if (!$coupon) {
return false;
}
// 2. 檢查是否已被使用 (假設 used_at 欄位為 null 代表尚未使用)
if ($coupon->used_at !== null) {
return false;
}
// 3. 檢查是否已過期 (假設 expires_at 是到期日)
if (Carbon::now()->gt($coupon->expires_at)) {
return false;
}
// 所有檢查都通過了!
return true;
}
/**
* Get the validation error message.
*
* @return string
*/
public function message()
{
// 給使用者一個通用的、安全的錯誤訊息
return '您輸入的優惠券代碼無效或已過期。';
}
}
步驟三:在控制器或 Form Request 中使用
現在,我們可以在需要的地方優雅地使用這個規則了。我強烈建議使用 Form Request 來組織你的驗證邏輯,這能讓你的 Controller 保持乾淨。這才是資深工程師的作法,別把所有邏輯都塞在 Controller 裡,那會變成一場災難!
<?php
namespace App\Http\Requests;
use Illuminate\Foundation\Http\FormRequest;
use App\Rules\IsValidCoupon; // 記得引入我們的規則
class ApplyCouponRequest extends FormRequest
{
public function rules()
{
return [
'coupon_code' => ['required', 'string', new IsValidCoupon()],
// 其他欄位...
];
}
}
看到了嗎?new IsValidCoupon(),就是這麼簡潔。我們把複雜的商業邏輯完美地封裝起來,控制器只需要專注於處理驗證通過後的請求即可。這不僅提高了程式碼的可讀性,也讓這個規則可以在任何地方被重複使用。
為什麼錯誤訊息要「故意講得模糊」?
注意上面 message() 回傳的是一句籠統的「無效或已過期」,而不是分別告訴使用者「這張券不存在」「這張券已被使用」「這張券已過期」。這是一個刻意的安全設計。
如果錯誤訊息太精確,等於免費送給攻擊者一個探測工具——他可以反覆送出不同代碼,從回應差異中推斷「哪些代碼真的存在」,進而暴力枚舉出有效優惠券。對外的訊息越模糊越好;真正詳細的失敗原因,應該寫進伺服器端的日誌(Log)裡供你自己除錯,而不是回給前端。
一個容易被忽略的坑:驗證通過不代表沒有競態問題
上面的規則檢查了「優惠券尚未被使用」,但要特別注意:驗證階段的檢查,和真正扣除優惠券的動作,是兩個分開的時間點。在高併發場景下(例如限量優惠券,很多人同時搶),可能出現兩個請求都通過了驗證、然後都去把同一張券標記為已使用的情況。
客製化驗證規則負責的是「擋掉一望即知無效的輸入」,而「同一張券只能被用一次」這種強一致性保證,最終仍要在實際寫入時,用資料庫交易(Transaction)、唯一索引或鎖(lock)來收尾。把驗證當成第一道篩子,但別把它當成唯一的保險。
新版 Laravel 的 Rule 介面有什麼不同?
上面範例實作的是 Illuminate\Contracts\Validation\Rule 介面,用的是 passes() 與 message() 兩個方法,這是長期以來最廣為人知的寫法,目前仍能運作。較新版本的 Laravel 另外提供了 ValidationRule 介面,把驗證集中在單一的 validate() 方法中,透過一個 $fail 回呼來丟出錯誤訊息。兩種寫法的核心觀念完全一致——把商業邏輯封裝成可重用的物件——差別只在方法簽章。若你正在維護既有專案,沿用現有寫法即可;若是全新專案,可參考你所用版本的官方文件選擇對應介面。
第二道防線:客製化中介層 — API 的忠實守門人
如果說 Validation 是檢查「信件內容」是否合規的審查員,那 Middleware 就是在信件送達前,檢查「信封」和「郵差身份」的守衛。它在請求生命週期的最前端,可以決定一個請求是否有資格繼續往下走。
什麼時候該手刻 Middleware?
- API 金鑰驗證:所有需要保護的 API 都應該有一個 Middleware 來檢查請求 Header 中的 API Key 是否有效。
- 權限控管:檢查登入的使用者是否具有特定角色(例如:管理員、編輯)才能存取某個路由。
- 請求日誌(Logging):記錄所有傳入的 API 請求,方便追蹤與除錯。
- 維護模式:在特定條件下,將網站導向維護頁面。
- 修改請求或回應:在請求送達控制器前,可以先加上特定的 Header;或在回應送回給使用者前,統一加上某些 Header。
先搞懂:Middleware 的「前置」與「後置」是什麼意思?
很多人以為 Middleware 只能在請求進來時做事,其實它的位置很靈活。關鍵在於你的邏輯寫在 $next($request) 的前面還是後面:
- 前置(before):把邏輯寫在呼叫
$next($request)之前。適合做「攔截」——金鑰驗證、權限檢查,不合格就直接擋下,根本不讓請求進到控制器。 - 後置(after):先取得
$response = $next($request),再對這個回應動手,最後才回傳。適合做「加工」——統一在回應掛上某個 Header、記錄回應狀態碼等。
理解這個前後關係,你就能寫出更有彈性的 Middleware,而不只是「擋掉或放行」這麼單純。下面的 API 金鑰範例,就是典型的「前置」用法。
實戰演練:打造一個「API 金鑰驗證」中介層
假設我們的 App 需要提供一組 API 給合作夥伴使用,我們得確保只有合法的請求才能存取。最常見的做法就是透過 HTTP Header 中的 X-API-KEY 來驗證。
步驟一:建立 Middleware
php artisan make:middleware EnsureApiKeyIsValid
這會在 app/Http/Middleware 資料夾下建立一個 EnsureApiKeyIsValid.php 檔案。
步驟二:撰寫處理邏輯
打開檔案,核心就在 handle() 方法。這個方法有兩個重要的參數:$request 和 $next。
$request:就是傳入的 HTTP 請求物件,我們可以從中取得 Header、輸入等資訊。$next:這是一個 Closure(閉包)。如果驗證通過,你必須呼叫$next($request),把請求「交棒」給下一個處理程序(可能是另一個 Middleware,或最終的控制器)。如果沒有呼叫它,請求就會在這裡被中止。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class EnsureApiKeyIsValid
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure $next
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
// 從 .env 檔案取得我們自己設定的合法 API Key
$validApiKey = config('services.api.key');
// 從請求的 Header 中取得傳入的 API Key
$submittedApiKey = $request->header('X-API-KEY');
// 進行比對
if (!$submittedApiKey || $submittedApiKey !== $validApiKey) {
// 如果驗證失敗,直接回傳 401 Unauthorized 錯誤,中斷請求
return response()->json(['message' => 'Unauthorized.'], 401);
}
// 驗證通過!交給下一個程序處理
return $next($request);
}
}
一個小提醒:千萬不要把 API Key 這種敏感資訊直接寫在程式碼裡,務必放在 .env 檔案中,然後透過 config() 輔助函數來讀取。
進階提醒:比對金鑰時的安全細節
上面用 !== 做字串比對,邏輯上完全正確,足以應付一般情境。但若你想再嚴謹一點,這裡有兩個值得知道的觀念:
- 時序攻擊(timing attack):一般的字串比較會在「第一個不相符的字元」就提前結束,理論上攻擊者可以從回應時間的細微差異反推金鑰。PHP 內建的
hash_equals()提供「定時比較」,比較兩個字串時不會因內容差異而提早返回,能降低這類風險。 - 狀態碼語意:金鑰錯誤回傳
401 Unauthorized(你沒通過身份驗證)是合理的;若是「身份有效但沒有權限存取此資源」,語意上更貼近403 Forbidden。把狀態碼用對,對串接你 API 的合作夥伴會友善很多。
這些都是錦上添花的細節,原本的範例已經能正常運作;要不要採用,看你的安全等級需求。
步驟三:註冊並使用 Middleware
Middleware 寫好了,但 Laravel 還不知道它的存在。我們需要去註冊它。在 Laravel 11 中,這件事變得更簡潔了,直接在 bootstrap/app.php 檔案中操作:
->withMiddleware(function (Middleware $middleware) {
// 給我們的 middleware 一個好記的別名
$middleware->alias([
'api.key' => \App\Http\Middleware\EnsureApiKeyIsValid::class,
]);
})
註冊完畢後,就可以在路由檔案(例如 routes/api.php)中保護我們的 API 了:
use App\Http\Controllers\PartnerController;
// 方法一:保護單一路由
Route::get('/partner/data', [PartnerController::class, 'getData'])->middleware('api.key');
// 方法二:保護一整個路由群組 (更推薦的作法)
Route::middleware(['api.key'])->prefix('partner')->group(function () {
Route::get('/data', [PartnerController::class, 'getData']);
Route::post('/update', [PartnerController::class, 'updateData']);
});
搞定!現在所有在那個群組內的 API,都必須在 Header 帶上正確的 X-API-KEY 才能存取。這道防線,堅固!
兩道防線怎麼搭配,才是企業級寫法?
把前面所有觀念串起來,一個健康的請求流程會像這樣,從外到內層層收斂:
- Middleware 先把關身份與資格:API 金鑰、登入狀態、角色權限。沒通過,請求在這裡就被擋下,連控制器都進不去。
- Form Request + 客製化驗證規則檢查資料內容:欄位格式、商業邏輯(如優惠券有效性)。這層保證進到控制器的資料都是「乾淨且合規」的。
- Controller 只專注於業務本身:因為前兩道防線已經幫它過濾掉非法請求與不合格資料,控制器可以假設「手上拿到的東西都是可信的」,邏輯自然清爽。
- 最終寫入時的一致性保證:對於限量、扣款這類不可重複的動作,在實際寫入資料庫的當下,用交易與鎖收尾,補上驗證階段無法保證的競態防護。
這套分層的精神,正是 DRY(Don't Repeat Yourself)原則的具體實踐:共通的把關邏輯抽到 Middleware,可重用的驗證封裝成 Rule Object,業務邏輯留在它該在的地方。
總結:打造可信賴的應用程式,從細節做起
身為工程師,我們寫的 Code 不只是要「能動」,更要「可靠」、「安全」、「可維護」。Laravel 驗證與 Middleware 客製化,正是實踐這些原則的關鍵武器。
客製化驗證讓我們能將複雜的商業規則封裝起來,保持 Controller 的簡潔;客製化中介層則為我們的應用程式建立了一道又一道的安檢門,將非法請求拒之門外。當你熟練地運用這兩者時,你就不再只是一個「會用框架」的開發者,而是一個真正懂得如何「架構應用程式」的工程師。
這條路很長,但每一步都值得。別再讓你的 Laravel API 裸奔了,現在就動手,為它穿上最堅實的盔甲吧!
延伸閱讀
常見問題
Laravel 的 Validation 和 Middleware 差別在哪?該用哪一個?
什麼情況該手刻 Laravel 客製化驗證規則,而不是用內建規則?
如何在 Laravel 建立客製化驗證規則?
驗證失敗的錯誤訊息為什麼要故意寫得模糊?
通過了優惠券驗證,就能保證一張券只被用一次嗎?
訂閱免費電子報
把 AI 自動化、企業系統設計與 WordPress / Laravel 開發的真實案例和可直接照做的技巧,整理成電子報寄給你。只寄精選內容、不灌垃圾信,一鍵就能退訂。