金牌給你,API 給我鎖!Laravel + JWT 打造銅牆鐵壁般的 API 認證機制全解析
哈囉,各位在程式碼海中奮鬥的夥伴們,我是浪花科技的資深工程師 Eric。今天不談 WordPress,我們來聊聊另一個後端開發的霸主——Laravel。寫後端最爽的時刻之一,莫過於你精心打造的 API 順利地吐出 JSON 資料,但最驚悚的時刻,也莫過於你發現這支 API 忘了上鎖,等於家裡大門敞開,任人來去自如,把你的寶貴資料當成自家後院在逛。
尤其在現在這個前後端分離、微服務、手機 App 當道的時代,一個安全、可靠、無狀態(Stateless)的 API 認證機制,已經不是「加分項」,而是「標準配備」。今天,我就要帶大家深入探討 Laravel API 開發中的王牌保鑣:JWT(JSON Web Tokens),從觀念到實戰,一步步教你如何為你的 API 建立起一道銅牆鐵壁。
JWT 到底是什麼?為什麼我的 Laravel API 需要它?
在我們動手寫 code 之前,身為一個囉嗦的工程師,我必須先確定大家都在同一個頻道上。很多新手開發者會問:「用 Laravel 內建的 Session 認證不行嗎?為什麼要搞得這麼複雜?」
問得好!傳統的 Session-based 認證,伺服器需要在某處(通常是檔案或資料庫)儲存使用者的登入狀態。當使用者登入後,伺服器會給他一個 Session ID 存在 Cookie 裡,之後每次請求,瀏覽器都會帶著這個 Cookie,伺服器再去核對 Session ID 來辨識使用者。這在傳統的網站(Monolithic Architecture)上運作得很好,但在現代 API 架構中,會遇到幾個痛點:
- 狀態依賴性 (Stateful): 伺服器必須儲存 Session 資訊,當你的服務需要水平擴展(Horizontal Scaling),也就是開好幾台伺服器來分擔流量時,Session 的同步會變成一場惡夢。
- 跨域問題 (CORS): API 通常會被不同網域的前端(例如 Vue/React SPA)或手機 App 呼叫,傳統基於 Cookie 的 Session 機制在處理跨域請求時會比較麻煩。
- 平台限制: 非瀏覽器的客戶端(如手機 App、物聯網裝置)不一定有完善的 Cookie 機制支援。
這時候,JWT 就颯爽登場了!JWT 的核心思想是「無狀態(Stateless)」。伺服器不需要儲存任何東西,所有必要的資訊都包含在一串加密過的字串(也就是 Token)裡,由客戶端自己保管。每次請求 API 時,客戶端只要附上這張「通行證」,伺服器驗證一下通行證的真偽,就能確認使用者身份。
拆解 JWT:一張數位身分證
一個 JWT Token 其實就是由三個部分組成的長字串,用點(.)分隔開來,分別是:
- Header (標頭): 記錄這個 Token 的基本資訊,例如類型(JWT)和使用的加密演算法(如 HMAC SHA256 或 RSA)。
- Payload (酬載): 存放實際要傳遞的資料,也就是「聲明(Claims)」。這裡面可以放一些不敏感的使用者資訊,例如使用者 ID、角色、過期時間等。(工程師囉嗦提醒:千萬不要把密碼之類的敏感資訊放在 Payload!)
- Signature (簽名): 這是最關鍵的部分。伺服器會用 `Header` + `Payload` 加上一個只有伺服器自己知道的「密鑰(Secret)」去進行加密,產生簽名。當伺服器收到 Token 時,會用同樣的方式再算一次簽名,如果跟 Token 裡的簽名一樣,就代表這張通行證是真的,而且中途沒有被竄改過。
簡單來說,伺服器發給你的 JWT,就像一張蓋了鋼印的數位身分證,只要鋼印(簽名)沒問題,伺服器就認得你。
實戰開始:用 tymon/jwt-auth 打造認證系統
觀念講完了,來點實際的吧!雖然 Laravel 生態系有 Sanctum(適合 SPA)和 Passport(完整 OAuth2 解決方案),但 `tymon/jwt-auth` 這個老牌套件在純粹的 JWT 認證場景中,依然非常強大且廣受歡迎。我們今天就用它來當作範例。
第一步:安裝與設定 Laravel 專案
首先,你需要一個全新的 Laravel 專案。打開你的終端機,讓我們開始施展魔法。
// 建立一個新的 Laravel 專案
composer create-project --prefer-dist laravel/laravel jwt-api-demo
// 進入專案目錄
cd jwt-api-demo
// 安裝 tymon/jwt-auth 套件
composer require tymon/jwt-auth:^1.0
安裝完畢後,我們要發布套件的設定檔,並產生一組專門用來簽發 JWT 的密鑰。
// 發布設定檔
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
// 產生 JWT 密鑰 (這會幫你在 .env 檔案中新增一行 JWT_SECRET)
php artisan jwt:secret
第二步:設定認證 Guard
接下來,我們要告訴 Laravel,當我們使用 `api` 這個認證管道(Guard)時,請使用 `jwt` 驅動程式。打開 `config/auth.php` 檔案,修改 `guards` 的部分:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// 將 'api' guard 的 driver 改成 'jwt'
'api' => [
'driver' => 'jwt',
'provider' => 'users',
],
],
第三步:修改 User 模型
為了讓我們的 `User` 模型能夠與 `tymon/jwt-auth` 協同工作,我們需要實作它的契約(Contract)。打開 `app/Models/User.php`:
<?php
namespace App\Models;
use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens; // 如果是新專案,這個可以留著或移除
use Tymon\JWTAuth\Contracts\JWTSubject; // 引入 JWTSubject
class User extends Authenticatable implements JWTSubject // 實作 JWTSubject
{
use HasFactory, Notifiable;
// ... (原本的屬性)
/**
* Get the identifier that will be stored in the subject claim of the JWT.
*
* @return mixed
*/
public function getJWTIdentifier()
{
return $this->getKey();
}
/**
* Return a key value array, containing any custom claims to be added to the JWT.
*
* @return array
*/
public function getJWTCustomClaims()
{
return [];
}
}
這兩個方法是套件規定必須實作的。`getJWTIdentifier()` 回傳了使用者的唯一識別碼(通常是主鍵 ID),`getJWTCustomClaims()` 則可以讓你加入自訂的資料到 Payload 中,我們暫時保持空白就好。
核心功能:撰寫註冊、登入與使用者資料 API
基礎建設都完成了,現在來蓋主建物吧!我們要建立一個 `AuthController` 來處理所有跟認證相關的邏輯。
第一步:規劃 API 路由
打開 `routes/api.php`,我們來定義需要的 API 端點:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\AuthController; // 引入我們的控制器
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function ($router) {
Route::post('register', [AuthController::class, 'register']);
Route::post('login', [AuthController::class, 'login']);
Route::post('logout', [AuthController::class, 'logout']);
Route::post('refresh', [AuthController::class, 'refresh']);
Route::get('me', [AuthController::class, 'me']);
});
這裡我們用了一個路由群組,統一加上 `/auth` 的前綴,方便管理。
第二步:建立 AuthController
在終端機執行 `php artisan make:controller AuthController`,然後把下面的程式碼貼進去 `app/Http/Controllers/AuthController.php`。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
use App\Models\User;
class AuthController extends Controller
{
public function __construct()
{
// 除了 register 和 login,其他 action 都需要經過 auth:api 中介軟體
$this->middleware('auth:api', ['except' => ['login', 'register']]);
}
public function login(Request $request)
{
$validator = Validator::make($request->all(), [
'email' => 'required|string|email',
'password' => 'required|string',
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
// 嘗試用 guard('api') 進行登入驗證
if (! $token = auth('api')->attempt($validator->validated())) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->createNewToken($token);
}
public function register(Request $request)
{
$validator = Validator::make($request->all(), [
'name' => 'required|string|between:2,100',
'email' => 'required|string|email|max:100|unique:users',
'password' => 'required|string|confirmed|min:6',
]);
if($validator->fails()){
return response()->json($validator->errors()->toJson(), 400);
}
$user = User::create(array_merge(
$validator->validated(),
['password' => Hash::make($request->password)]
));
return response()->json([
'message' => 'User successfully registered',
'user' => $user
], 201);
}
public function logout()
{
auth('api')->logout();
return response()->json(['message' => 'Successfully logged out']);
}
public function refresh()
{
return $this->createNewToken(auth('api')->refresh());
}
public function me()
{
return response()->json(auth('api')->user());
}
protected function createNewToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60, // 單位是秒
'user' => auth('api')->user()
]);
}
}
這段程式碼涵蓋了:
- `login()`: 驗證使用者帳密,成功後呼叫 `createNewToken()` 來產生並回傳 Token。
- `register()`: 驗證註冊資料,並建立新使用者。
- `logout()`: 將當前的 Token 加入黑名單,使其失效。
- `refresh()`: 用舊的(但還沒過期的)Token 換一個新的 Token,達到延長登入時間的效果。
- `me()`: 取得當前登入使用者的資訊,這個路由會被 `auth:api` 中介軟體保護。
- `createNewToken()`: 一個輔助函式,用來把 Token 和其他相關資訊組合成標準的 JSON 格式回傳。
上鎖!用 Middleware 保護你的重要資料
在 `AuthController` 的建構子中,我們已經用了 ` $this->middleware(‘auth:api’, [‘except’ => [‘login’, ‘register’]]);`。這行的意思就是:「這個 Controller 裡的所有方法,都必須先通過 `auth:api` 這個警衛的檢查,除了 `login` 和 `register` 這兩個訪客通道之外。」
現在,你可以用 Postman 或任何 API 測試工具來試試看了!
- 先打 `POST /api/auth/register` 註冊一個新帳號。
- 再打 `POST /api/auth/login` 用剛剛的帳號登入,你會收到一組 `access_token`。
- 接著,試著打 `GET /api/auth/me`,你會收到 `401 Unauthorized` 的錯誤,因為你沒帶通行證。
- 最後,在你的請求 Header 中加入 `Authorization`,值設為 `Bearer YOUR_ACCESS_TOKEN`(把 `YOUR_ACCESS_TOKEN` 換成你登入時拿到的那串 Token),再打一次 `GET /api/auth/me`,你就會看到成功回傳的使用者資料了!
看吧!你的 API 已經成功上鎖了,只有手上握有合法 Token 的人才能存取受保護的資源。
工程師的小囉嗦:安全與最佳實踐
功能做完了,但身為資深工程師,有些安全性的事情一定要再三叮嚀:
- 全程 HTTPS: JWT 本身雖然有簽名防竄改,但 Payload 的內容預設只是 Base64 編碼,很容易被解開。如果用 HTTP 傳輸,等於是在路上裸奔,Token 很容易被中間人攔截。所以,請務必、絕對、一定!要使用 HTTPS。
- Token 存哪裡: 在前端,Token 要存在哪是個大學問。存在 `localStorage` 最簡單,但有被 XSS(跨站腳本攻擊)偷走的風險。存在 `HttpOnly` 的 Cookie 裡可以防範 XSS,但又需要處理 CSRF(跨站請求偽造)的問題。這沒有標準答案,需要根據你的應用場景權衡風險。
- 保持 Token 的生命週期短暫: 設定一個合理的過期時間(例如 15 分鐘或 1 小時),並搭配 `refresh` 機制,可以在安全性和使用者體驗之間取得平衡。
Laravel API 開發與 JWT 認證的結合,提供了一個非常強大且彈性的解決方案,讓你能夠安心地建構現代化的應用程式。雖然初期設定有點繁瑣,但一旦架構起來,後續的開發會非常順暢。這就像蓋房子,地基打得穩,樓才蓋得高!
希望這篇從觀念到實戰的完整教學,能幫助你順利為你的 Laravel API 加上最堅實的防護。如果你在實作過程中遇到任何問題,或是對於企業級的 API 架構、系統整合有更深入的需求,都歡迎隨時與我們浪花科技的團隊聊聊。
延伸閱讀
- 別再讓你的 API 裸奔!資深工程師的 Laravel Webhook 安全實戰:從設計到簽名驗證,打造滴水不漏的自動化橋樑
- 別再寫出連自己都看不懂的 API!資深工程師的 WordPress REST API 設計原則終極指南
- 前後端分離還是大一統?Laravel + Vue / React 整合全攻略,資深工程師帶你選對路
我們不只會寫程式,更擅長將複雜的技術轉化為推動您事業成長的動力。立即填寫表單,與我們的技術顧問聊聊您的專案吧!
常見問題 (FAQ)
Q1: JWT 和傳統 Session 認證最大的差別是什麼?
最大的差別在於「狀態」。傳統 Session 是「有狀態的 (Stateful)」,伺服器需要儲存每個使用者的登入資訊。而 JWT 是「無狀態的 (Stateless)」,所有需要的認證資訊都包含在 Token 本身,由客戶端儲存,伺服器本身不保留任何狀態。這使得基於 JWT 的應用程式更容易水平擴展和解耦。
Q2: tymon/jwt-auth、Sanctum 和 Passport 我該怎麼選擇?
這是一個很好的問題!簡單來說:
- tymon/jwt-auth: 適合需要一個純粹、功能完整的 JWT 解決方案的場景,特別是當你的 API 消費者非常多樣化時(例如網頁、手機 App、第三方服務)。
- Laravel Sanctum: 最適合用在 SPA(單頁應用程式,如 Vue/React)或手機 App 與自家 API 的溝通。它提供了一種輕量級的認證方式,開發體驗與 Laravel 緊密整合。
- Laravel Passport: 當你需要一個完整的 OAuth2 伺服器時才選擇它。例如,如果你要讓第三方應用程式可以安全地存取你使用者的資料(像是「使用 Google 登入」那樣),Passport 就是你的不二之選。
Q3: Token 該存在客戶端的哪裡比較安全?localStorage 還是 Cookie?
這是一個經典的權衡問題:
- localStorage: 存取方便,但容易受到 XSS (跨站腳本攻擊) 的威脅。如果攻擊者能在你的網站上執行惡意 JavaScript,他就能讀取 localStorage 裡的 Token。
- HttpOnly Cookie: JavaScript 無法讀取 `HttpOnly` 屬性的 Cookie,可以有效防禦 XSS。但它需要處理 CSRF (跨站請求偽造) 的風險,雖然可以透過 SameSite 屬性等方式緩解,但需要額外的設定。
一般來說,如果你的網站對 XSS 有足夠的防護,`localStorage` 是個方便的選擇。如果安全是最高考量,`HttpOnly Cookie` 加上 CSRF 防護會是更穩健的方案。






