你的 API 只是「能動」還是「安全」?Laravel + JWT 深度實戰,打造駭客也搖頭的認證機制
嗨,我是浪花科技的資深工程師 Eric。在開發的世界裡,我們最常聽到的一句話大概就是:「太好了,API 在 Postman 上能動了!」然後呢?然後就急著跟前端或 App 的同事說可以串接了。但身為一個在鍵盤上打滾多年的老司機,我得囉嗦一句:能動,跟安全,是兩碼子事。尤其是在這個前後端分離、手機 App 滿天飛的時代,API 就像你家的數位大門,如果不上鎖,等於是邀請小偷進來開派對。
傳統的 Session-Cookie 機制在無狀態 (Stateless) 的 RESTful API 架構下顯得有點力不從心,特別是當你的客戶端不只是瀏覽器,還包含 iOS、Android App 時。這時候,JWT (JSON Web Tokens) 就像一把萬能鑰匙,為我們開啟了現代化、無狀態認證的大門。今天,就讓我帶你從頭到尾,手把手用 Laravel 打造一個真正安全、可靠的 JWT 認證機制。
JWT 到底是什麼?三分鐘搞懂它的「數位身分證」結構
在我們開始寫 Code 之前,花點時間搞懂原理絕對是值得的。很多人聽到 JWT 就覺得很複雜,其實你大可以把它想像成一張經過加密防偽的「數位身分證」。這張身分證由三個部分組成,用點 (.) 隔開,分別是:
- Header (標頭): 記載這張身分證的基本資訊,比如類型是 JWT,以及用哪種加密演算法來產生簽名 (例如:HS256)。
- Payload (酬載): 這是身分證的核心內容,放了一些我們想傳遞的資訊。例如,這個 token 是發給誰的 (使用者 ID)、何時發的、何時過期等等。有個工程師的小提醒:Payload 裡的資料只是經過 Base64 編碼,並沒有加密,所以千萬不要放密碼之類的敏感資料!
- Signature (簽章): 這是最重要的防偽標籤。它會把 Header 和 Payload 再加上一個只有伺服器知道的「密鑰 (Secret Key)」一起加密。當伺服器收到一個 JWT 時,會用同樣的方式再算一次簽章,如果跟收到的簽章一樣,就代表這張「身分證」沒有被偽造或竄改過。
簡單來說,JWT 的核心精神就是「無狀態」和「自我驗證」。伺服器不需要在資料庫裡存一個 session 來記錄使用者登入狀態,只要驗證 token 的簽章合法性,就能信任這個請求,這對於系統的擴展性非常有幫助。
工欲善其事,必先利其器:設定 Laravel 開發環境
理論講完了,我們來動手吧!我假設你已經有一個基本的 Laravel 專案在手上。接下來,我們要安裝一個在 Laravel 社群中非常受歡迎的 JWT 套件:tymon/jwt-auth。
首先,打開你的終端機,進入專案目錄,執行以下指令安裝套件:
composer require tymon/jwt-auth
安裝完成後,我們需要發布它的設定檔,這樣才能進行客製化。執行:
php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"
最後,也是最關鍵的一步,產生 JWT 會用到的密鑰。這個密鑰會被寫入你的 .env 檔案中,絕對要好好保管,不能外洩!
php artisan jwt:secret
看到 JWT secret set successfully. 的訊息就代表你的前置作業完成了。是不是很簡單?
實戰開始!打造 JWT 認證的 API 端點
環境準備好了,接下來就是重頭戲:建立登入、登出、取得使用者資料等核心的認證 API。
第一步:讓你的 User Model 支援 JWT
我們要告訴 Laravel 的 User Model,它現在需要扮演一個「JWT 主體」的角色。打開 app/Models/User.php,我們需要做兩件事:
- 引用
Tymon\JWTAuth\Contracts\JWTSubject這個 interface。 - 實作 interface 要求的兩個方法:
getJWTIdentifier()和getJWTCustomClaims()。
聽起來很抽象?直接看 Code 就懂了。這就是身為工程師的浪漫,Code is law!
<?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 HasApiTokens, 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() 是用來定義要存在 JWT payload 裡的 `sub` (subject) 欄位,通常就是使用者的 primary key。getJWTCustomClaims() 則可以讓你加入自訂的資料到 payload 中,例如使用者角色,但目前我們先保持空白。
第二步:設定 API 的認證驅動
接著,我們要告訴 Laravel,當我們使用 API 路由時,預設的認證方式應該是 JWT。打開 config/auth.php,找到 guards 這個陣列,並修改 api 的設定:
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'jwt', // 將 driver 改成 jwt
'provider' => 'users',
],
],
這一步超級重要,很多新手卡關都是因為忘了改這裡,導致 Laravel 不知道該用 JWT 來驗證 API 請求。
第三步:建立認證控制器與路由
萬事俱備,只欠東風。我們來建立一個專門處理認證邏輯的 Controller。
php artisan make:controller Api/AuthController
然後,打開 routes/api.php,定義我們的 API 路由:
<?php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\AuthController;
Route::group(['middleware' => 'api', 'prefix' => 'auth'], function () {
Route::post('login', [AuthController::class, 'login']);
Route::post('logout', [AuthController::class, 'logout']);
Route::post('refresh', [AuthController::class, 'refresh']);
Route::post('me', [AuthController::class, 'me']);
});
最後,我們來把 AuthController.php 的邏輯補上:
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class AuthController extends Controller
{
public function __construct()
{
// 除了 login 以外的 function 都需要經過 auth:api 這個 middleware
$this->middleware('auth:api', ['except' => ['login']]);
}
public function login()
{
$credentials = request(['email', 'password']);
if (! $token = auth('api')->attempt($credentials)) {
return response()->json(['error' => 'Unauthorized'], 401);
}
return $this->respondWithToken($token);
}
public function me()
{
return response()->json(auth('api')->user());
}
public function logout()
{
auth('api')->logout();
return response()->json(['message' => 'Successfully logged out']);
}
public function refresh()
{
return $this->respondWithToken(auth('api')->refresh());
}
protected function respondWithToken($token)
{
return response()->json([
'access_token' => $token,
'token_type' => 'bearer',
'expires_in' => auth('api')->factory()->getTTL() * 60 // 預設是一小時
]);
}
}
在這裡,login 方法會驗證使用者帳密,成功後回傳一個 token。me、logout、refresh 等方法,我們在建構子中加入了 auth:api middleware,代表這些路由都必須在請求的 Header 中帶上有效的 JWT 才能存取。
銅牆鐵壁:用 Middleware 保護你的 API 路由
現在你有了一個完整的認證流程,但要怎麼保護其他的 API 呢?例如,一個只有登入會員才能看到的訂單列表。這就是 Middleware 發揮作用的時候了。
在你的 routes/api.php 中,只要將需要保護的路由群組起來,並套用 auth:api middleware 即可:
Route::group(['middleware' => 'auth:api'], function () {
// 這裡放所有需要登入才能存取的 API
Route::get('/orders', [OrderController::class, 'index']);
Route::post('/profile', [ProfileController::class, 'update']);
});
就這麼簡單!現在,如果有人想存取 /api/orders 卻沒有在 HTTP Header 中提供有效的 Bearer Token,Laravel 就會自動回傳一個 401 Unauthorized 的錯誤,把不速之客擋在門外。
JWT 安全性的小囉嗦:工程師的自我修養
又到了工程師的小囉嗦時間。實現功能只是第一步,確保安全才是專業的體現。關於 JWT,有幾點你必須放在心上:
- 全程 HTTPS: 我再強調一次,JWT 的 Payload 只是編碼,不是加密。如果你的 API 跑在 HTTP 上,那 token 就跟明文一樣,在網路傳輸中可能被攔截。所以,請務必、一定、絕對要使用 HTTPS。
- 設定合理的 Token 過期時間: Token 就像牛奶,有保存期限。建議 access_token 的期限不要太長,例如 1 小時或 15 分鐘。搭配 refresh token 機制,可以在安全性和使用者體驗之間取得平衡。
- 安全存放 Token: 在前端,Token 該放哪裡也是一門學問。放在 `localStorage` 雖然方便,但有被 XSS 攻擊竊取的風險。放在 `HttpOnly` 的 Cookie 中會相對安全,但需要處理 CSRF 的問題。這是一個權衡,你需要根據你的應用場景做決定。
- 密鑰的保管:
.env裡的JWT_SECRET是你的身家性命,絕對不能洩漏,也不能跟著 Git 一起 commit 上去。
恭喜你!跟著這篇文章走完,你已經成功地為你的 Laravel 專案建立了一套穩固、安全且可擴展的 API 認證系統。這不僅僅是完成一個功能,更是為你的應用程式打下了堅實的地基。從此以後,無論是開發 SPA 網站還是手機 App,你都能自信地提供一個安全的後端服務。
當然,技術的世界學無止境,從 Token 的黑名單機制到更複雜的 RBAC (Role-Based Access Control) 權限管理,還有很多可以深入探索的地方。但今天,你已經邁出了最重要的一步。
相關閱讀
- 別再讓你的 API 裸奔!資深工程師的 Laravel Webhook 安全實戰:從設計到簽名驗證,打造滴水不漏的自動化橋樑
- Laravel 門神不好當?從自訂驗證到 Middleware,打造滴水不漏的 API 防線
- 肥 Controller 瘦不下來?Laravel 後台架構終極對決:Repository vs. Action 模式,資深工程師帶你選對屠龍刀!
如果你在開發 Laravel 或 WordPress 專案時遇到了任何瓶頸,或是有更複雜的系統架構、API 串接需求,浪花科技的團隊擁有多年的實戰經驗,可以為你提供專業的諮詢與開發服務。別猶豫,立即聯繫我們,讓我們一起打造出色的數位產品!
常見問題 (FAQ)
Q1: JWT 跟傳統的 Session 認證有什麼最大的不同?
最大的不同在於「狀態」。Session 是有狀態的 (Stateful),伺服器需要儲存每個使用者的登入資訊,通常是存在檔案或資料庫裡。而 JWT 是無狀態的 (Stateless),所有需要的驗證資訊都包含在 token 本身,伺服器不需要額外儲存,這讓系統在水平擴展(增加更多伺服器)時變得非常容易。
Q2: 在前端,我應該把 JWT 存在哪裡比較好?localStorage 還是 Cookie?
這是一個經典的權衡問題。存 `localStorage` 的好處是簡單、易於用 JavaScript 操作,但缺點是容易受到 XSS (跨站腳本攻擊) 竊取。存 `HttpOnly` Cookie 的好處是 JavaScript 無法讀取,可以有效防止 XSS,但需要處理 CSRF (跨站請求偽造) 的問題。一般來說,如果安全性要求非常高,會推薦使用 `HttpOnly` Cookie 搭配 CSRF Token 保護。
Q3: 如果我不小心讓 JWT_SECRET 密鑰外洩了,會發生什麼事?
這是最糟的情況!如果密鑰外洩,攻擊者就可以用這個密鑰簽發任意內容的 JWT token。他可以偽造成任何使用者的身份,甚至可以把自己變成系統管理員,對你的系統造成毀滅性的打擊。所以,請像保護你的銀行密碼一樣保護你的 JWT 密鑰。
Q4: 使用者登出後,他的 JWT 在過期前不就還能用嗎?該怎麼辦?
這是 JWT 無狀態特性帶來的一個問題。解決方案是引入「黑名單 (Blacklist)」機制。當使用者登出時,將他的 JWT 加入到一個有過期時間的快取中(例如 Redis)。之後每次驗證 token 時,除了檢查簽章和過期時間,還要多一步檢查這個 token 是否在黑名單裡。tymon/jwt-auth 套件本身就有支援這個機制,可以深入研究一下它的設定。






