「開新專案要不要順便裝認證?裝哪一套?」這題的標準答案在 Laravel 12 之後整個換了:Breeze 和 Jetstream 停更、官方文件連介紹頁都撤掉了,取而代之的是一套全新的 Starter Kit 系統,還多了一個叫 WorkOS AuthKit 的選項可以讓你完全不用自己處理密碼管理。今天我們用新版 Starter Kit 把 blog-app 的認證系統建起來,並且深入到「密碼規則怎麼客製化」「登入失敗要不要限流」這種實務上一定會遇到的細節。
如果你看過第一季,那時候我介紹的第三方專案產生器「Laravel initializer」現在已經 404 了,裡面提到的 Breeze / Jetstream / Fortify 三選一組合也不再是標準答案。官方文件目前的說法是:
Laravel 12 引進了全新的 React、Svelte、Vue、Livewire 官方 Starter Kit(含選配的 WorkOS AuthKit 認證),Laravel Breeze 和 Laravel Jetstream 從此不再收到更新。
兩層意思要分清楚:
composer require laravel/breeze 目前也還能裝——但官方不會再加新功能或修 bug,長期而言你會希望遷移。這是個實務上常被問到、但答案沒那麼絕對的問題。幾個判斷點:
LoginController/RegisterController 的地方,因為新 Kit 的架構(Fortify 分離前後端)跟 Breeze(前後端邏輯常常寫在同一個 Controller)並不完全對應。這通常是一個獨立的技術任務,需要抓時間評估,不建議跟其他功能開發混在一起做。設計哲學跟 Breeze 一樣(給你完整原始碼、不是黑盒套件),但技術堆疊全面升級:
| Starter Kit | 前端技術 | UI 元件庫 |
|---|---|---|
| React | Inertia 3 + React 19 + TypeScript | shadcn/ui |
| Svelte | Inertia 3 + Svelte 5 + TypeScript | shadcn-svelte |
| Vue | Inertia 3 + Vue 3 Composition API + TypeScript | shadcn-vue |
| Livewire | Livewire 4 + Blade | Flux UI |
四種 Kit 有一個共同點:認證邏輯全部交給 Laravel Fortify 處理,Fortify 提供登入、註冊、忘記密碼、Email 驗證、雙因子驗證(2FA)等一整套 route/controller 邏輯,四種 Kit 只是在這個共同的認證後端上面,包了不同的前端外皮。

Breeze/新版 Kit 跟 Jetstream/傳統認證套件最大的哲學差異,在於程式碼的所有權。傳統套件(例如早期的 laravel/ui)把認證邏輯打包成一個黑盒子,裝進 vendor/ 目錄,你要客製化行為得靠套件開放的設定選項或事件監聽,改不動的地方就真的改不動。Starter Kit 走的是完全相反的路線:安裝的當下,所有 Controller、View、路由檔案都直接複製進你的 app/、resources/、routes/ 目錄,變成「你自己寫的程式碼」——裝完之後,Kit 本身的套件依賴其實很少,你看到的每一行認證邏輯都可以自由修改,不需要考慮「這樣改會不會被下次套件更新覆蓋掉」。
這個設計哲學的代價是:Starter Kit 不會隨著 Laravel 版本自動升級認證邏輯,因為那些程式碼已經是你的了,不再是套件的一部分。如果之後 Fortify 或前端框架有安全性更新,需要你自己比對變更、手動套用到你已經客製化過的程式碼裡——這是換取完全控制權必然要付出的維護成本,跟「裝一個黑盒套件、等它發新版」的模式是完全不同的取捨。
安裝流程現在全部透過官方 laravel CLI 工具完成——今天就是這系列真正建立 blog-app 的一天(Day 2 那個 hello-laravel 只是用來驗收環境的拋棄式專案,確認過環境沒問題就可以刪掉了):
composer global require laravel/installer
laravel new blog-app
執行 laravel new 之後,安裝程式會用互動式問答讓你選擇:
npm install

選好之後,安裝程式會自動跑完整套設定,完成後你只需要:
cd blog-app
npm install && npm run build
composer run dev
composer run dev 是 Starter Kit 附帶的一個方便指令,會同時啟動 PHP 開發伺服器、Vite 前端編譯監看、Queue Worker 等多個開發用行程。這背後其實是 composer.json 的 scripts 區塊定義的一個組合指令,如果你想知道它實際做了什麼,可以打開 composer.json 查看 dev 這個 script 的內容——通常會看到它用 npx concurrently 同時起了好幾個行程,這也是為什麼你只要按一次 Ctrl+C 就能把所有開發用行程一起關掉。
這個系列選用 Livewire Starter Kit,理由很單純:後續 30 天的內容重心是後端概念(Eloquent、Middleware、Queue、Service Container……),Livewire 讓我們可以完全留在 Blade/PHP 的世界裡示範互動式介面,不需要額外解釋 React/Vue 的前端知識。如果你偏好 React/Vue/Svelte 生態,後續文章的後端程式碼一樣適用,只是畫面呈現的部分需要自行對應轉換。
不管你選哪個前端 Kit,實際處理登入/註冊邏輯的都是 Fortify。裝好專案後,你可以在 config/fortify.php 看到一個 features 陣列,決定啟用哪些認證功能:
use Laravel\Fortify\Features;
'features' => [
Features::registration(),
Features::resetPasswords(),
Features::emailVerification(),
Features::twoFactorAuthentication([
'confirm' => true,
'confirmPassword' => true,
]),
],
想關掉某個功能(例如不開放自行註冊,帳號都由後台建立),把對應那行註解掉或刪除即可。如果你用的是 React/Svelte/Vue Kit,記得同步把前端頁面裡引用到該功能路由的程式碼也移除,因為這幾個 Kit 用 Wayfinder 在編譯期產生型別安全的路由定義,引用到不存在的路由會導致建置失敗——這是新版 Kit 特有的細節,Livewire Kit 因為是伺服器端渲染,沒有這個限制。
真正處理「怎麼建立一個新使用者」「密碼規則長怎樣」的程式碼,放在 app/Actions/Fortify/ 目錄下:
| 檔案 | 用途 |
|---|---|
CreateNewUser.php |
驗證輸入並建立新使用者 |
ResetUserPassword.php |
驗證並更新使用者密碼 |
UpdateUserPassword.php |
已登入使用者主動更改密碼 |
UpdateUserProfileInformation.php |
更新姓名/Email 等基本資料 |
PasswordValidationRules.php |
定義密碼驗證規則 |
如果 blog-app 之後想在註冊表單加一個「暱稱」欄位,直接改 CreateNewUser::create() 方法即可——認證邏輯集中在這個目錄,不用到處找。
假設 blog-app 的營運方要求密碼規則要比預設更嚴格(至少 10 個字元、必須包含符號),打開 PasswordValidationRules.php:
// app/Actions/Fortify/PasswordValidationRules.php
trait PasswordValidationRules
{
protected function passwordRules(): array
{
return [
'required',
'string',
Password::min(10)
->letters()
->mixedCase()
->numbers()
->symbols()
->uncompromised(), // 檢查是否出現在已知外洩密碼資料庫
'confirmed',
];
}
}
Password::min(10)->letters()->mixedCase()->numbers()->symbols() 這條鏈式呼叫是 Laravel 內建的 Illuminate\Validation\Rules\Password 規則物件,uncompromised() 這個方法特別值得認識——它會透過 Have I Been Pwned 的 k-Anonymity API,檢查使用者輸入的密碼是否出現在已知的資料外洩清單裡(傳輸過程只送出密碼雜湊值的前 5 碼,不會外洩完整密碼),如果命中就拒絕這組密碼。這個規則不需要額外的第三方帳號或金鑰,是提升密碼安全性成本最低的一步。
這個 passwordRules() trait 同時被 CreateNewUser(註冊)和 UpdateUserPassword(改密碼)共用,改一處就能同步套用到所有需要密碼驗證的情境,這是這套架構刻意設計成 trait 而非各自硬寫的原因。
Features::twoFactorAuthentication() 啟用後,使用者可以在個人設定頁面開啟 2FA,流程大致是:
confirm: true 這個選項代表使用者必須輸入一次驗證碼確認設定成功,才算真正啟用(避免使用者掃了 QR Code 但沒有正確設定好 App,之後被鎖在帳號外面)。confirmPassword: true 代表使用者要開啟/關閉 2FA 這種敏感操作前,系統會要求重新輸入一次密碼確認身份(即使使用者已經登入中)。這一整套流程完全由 Fortify 處理,blog-app 不需要自己寫任何 TOTP(Time-based One-Time Password)演算法邏輯,這也是選擇官方認證方案而不是自己從頭刻的主要理由之一:2FA 這類牽涉密碼學細節、稍有差錯就是資安漏洞的功能,交給經過廣泛審查的官方實作遠比自己寫更值得信賴。

Fortify 預設已經幫你設定好登入失敗的節流保護,定義在 app/Providers/FortifyServiceProvider.php(或視 Starter Kit 版本,可能整合進 AppServiceProvider):
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
RateLimiter::for('login', function (Request $request) {
$throttleKey = Str::transliterate(Str::lower($request->input(Fortify::username())).'|'.$request->ip());
return Limit::perMinute(5)->by($throttleKey);
});
這條規則同時考慮了「使用者名稱」跟「IP 位址」兩個維度組成節流的 key,用意是防止兩種不同的暴力破解手法:針對單一帳號的密碼字典攻擊(同一個帳號被大量嘗試不同密碼)跟針對單一 IP 的帳號列舉攻擊(同一個來源嘗試大量不同帳號)。這條規則跟 Day 7 會講到的 RateLimiter::for() 是同一套機制,這裡先讓你知道 Fortify 已經內建了這層保護,不需要自己額外實作。
如果你的應用需要企業級的 SSO、想支援 Passkey(無密碼登入)、或想要「一鍵用 Google/Microsoft/GitHub/Apple 登入」,新版 Starter Kit 有一個內建的 WorkOS AuthKit 變體。安裝時選擇 WorkOS 選項,Laravel 就會把整個認證流程委託給 WorkOS 這個第三方服務,你的應用程式完全不需要處理密碼儲存。
啟用後需要到 .env 補上三個變數(值來自 WorkOS 後台):
WORKOS_CLIENT_ID=your-client-id
WORKOS_API_KEY=your-api-key
WORKOS_REDIRECT_URL="${APP_URL}/authenticate"
WorkOS 對月活躍使用者 100 萬以下的應用程式提供免費額度,適合新創或個人專案先用免費方案把 SSO/Passkey 這種通常很花工程時間的功能直接外包掉。
啟用 WorkOS AuthKit 之後,blog-app 的使用者資料模型還是原本的 User,但密碼欄位基本上不會被使用——使用者的身份驗證完全發生在 WorkOS 的託管頁面,你的應用程式只負責在使用者驗證成功後,接收 WorkOS 回傳的身份資訊並建立/更新對應的本地 User 紀錄(這個流程通常叫做「Just-In-Time Provisioning」)。這代表如果你之後想要「先用 WorkOS,之後又想加回傳統密碼登入」,需要額外處理「這個帳號到底該用哪種方式驗證」的邏輯,不是單純切換設定就好,這是選擇 WorkOS 前值得先想清楚的架構決策。
兩者容易搞混,先講清楚定位差異避免選錯:Socialite 適合「單純想加個 Google/GitHub 登入按鈕」的情境,WorkOS AuthKit 適合需要企業 SSO、Passkey、或多種登入方式一次到位的情境。WorkOS 不是要取代 Socialite,兩者可以並存,依需求選擇(Socialite 的實際用法 Day 28 會示範)。
更具體一點的判斷方式:如果你的產品是面向一般消費者(B2C,例如 blog-app 這種部落格平台),使用者多半只是想快速用既有的 Google 帳號登入,不需要 SSO 這種企業級功能,Socialite 更輕量、實作也更直觀。如果你的產品是面向企業客戶(B2B SaaS),客戶公司的 IT 部門通常會要求「員工登入你的系統要走公司自己的 Okta/Azure AD」,這種情境正是 WorkOS AuthKit 設計要解決的問題——它原生支援 SAML/OIDC 這些企業 SSO 標準協定,而 Socialite 主要是針對消費級社群平台(Google、GitHub、Facebook 等)的整合,兩者的目標客群跟技術複雜度完全不在同一個量級。
laravel new blog-app --breeze 之類的參數:這是舊版 installer 的用法,新版 installer 是互動式問答,不再用這類 flag 指定 Breeze/Jetstream。如果你想用社群維護的其他 Starter Kit,正確參數是 laravel new my-app --using=vendor/package-name。config/fortify.php 卻編譯失敗:多半是忘記同步刪除前端頁面對已停用功能路由的引用,前面提過的 Wayfinder 型別安全路由機制會在建置期直接報錯,這其實是件好事——它幫你在部署前就抓到不一致,而不是等使用者點到壞掉的連結。passwordRules() 卻忘記現有使用者的舊密碼不受影響:加嚴密碼規則只會套用到「之後」的註冊與改密碼流程,資料庫裡既有使用者的舊密碼雜湊不會被強制要求重新設定,如果你的產品規範要求全體使用者跟進新規則,需要另外設計強制改密碼的流程(例如下次登入時導向改密碼頁面),這不是 Fortify 自動幫你做的事。uncompromised() 規則在本機開發環境噴出網路錯誤:這個規則需要連外呼叫 Have I Been Pwned 的 API,如果本機環境沒有對外網路(例如某些企業內網開發環境),驗證會失敗。開發/測試環境可以考慮用 App::environment() 判斷條件式地跳過這條規則,正式環境保留。confirmPassword: true,卻讓使用者在 API 情境下卡住:如果 blog-app 之後有原生 App 或 SPA 透過 API 呼叫這些敏感端點,confirmPassword 這種依賴 Session 重新確認密碼的機制,需要對應調整成 API 友善的實作方式(例如另外設計一個帶密碼的請求端點),不能直接假設前端一定有一個「請再輸入一次密碼」的網頁表單可以用。新版 Starter Kit 用 Fortify 統一了認證後端邏輯,前端則開放 React/Svelte/Vue/Livewire 四選一,外加選配的 WorkOS AuthKit 處理企業級認證需求。今天除了認識整體架構,也實際走過密碼規則客製化、2FA 使用者流程、登入節流保護這幾個實務上一定會遇到的細節。Breeze/Jetstream 沒有被砍掉,但已經是「維護模式」的舊技術,新專案沒有特別理由的話直接用新版 Starter Kit。我們的 blog-app 從今天起就是一個用 Livewire Starter Kit 建立、Fortify 驅動認證的專案。
站在巨人的肩膀上,才能看得更遠 — 牛頓
Day 5 打開 blog-app 的目錄結構,你會發現少了很多熟悉的檔案——Kernel.php 去哪了?答案都在 bootstrap/app.php 這個新骨架的核心裡。