API 沒上鎖,等於家裡沒關門!Laravel JWT 終極實戰,手把手打造無狀態認證金鑰

2025/09/15 | Laravel技術分享

API 沒上鎖,等於家裡沒關門!Laravel JWT 終極實戰,手把手打造無狀態認證金鑰

嗨,大家好,我是浪花科技的資深工程師 Eric。在現在這個前後端分離、微服務、手機 App 當道的世界,API (應用程式介面) 就像是服務的心臟,負責所有資料的溝通與流動。但如果這個心臟的大門沒上鎖,任何人都能隨意進出,那後果可不堪設想。這就是為什麼 API 認證(Authentication)如此重要的原因。

過去我們習慣用 Session 來處理使用者登入狀態,但在現代化的架構下,它顯得有些笨重且缺乏彈性。今天,我就要帶大家深入探討目前 API 開發的主流認證機制 —— JWT (JSON Web Tokens),並手把手教你如何在 Laravel 框架中,打造一個安全、高效、可擴展的「無狀態」認證系統。準備好了嗎?泡杯咖啡,我們開始囉!

為什麼你的 API 需要「無狀態」認證?告別傳統 Session 的包袱

在我們動手寫 Code 之前,身為一個囉嗦的工程師,我必須先讓大家理解「為什麼」我們要選擇 JWT,而不是沿用過去熟悉的 Session-Cookie 機制。知其然,更要知其所以然,這樣才能在未來面對不同場景時,做出最正確的技術決策。

Session-based 認證的困境

傳統的 Session 認證流程大概是這樣:

  • 使用者登入成功後,伺服器會建立一個 Session 檔案或紀錄(通常存在檔案系統或 Redis 中),裡面存放著使用者的資訊。
  • 伺服器同時會回傳一個 Session ID 給瀏覽器,並存放在 Cookie 中。
  • 之後的每一次請求,瀏覽器都會自動帶上這個存有 Session ID 的 Cookie。
  • 伺服器收到請求後,根據 Session ID 找到對應的 Session 檔案,確認使用者是誰,然後處理請求。

這在單一主機的傳統網站上運作得很好,但在現代架構下卻會遇到幾個致命問題:

  • 擴展性問題 (Scalability): 當你的網站流量變大,需要多台伺服器做負載均衡 (Load Balancing) 時,問題就來了。使用者的第一次請求可能送到 A 伺服器,Session 檔案也建立在 A 伺服器;但下一次請求可能被分配到 B 伺服器,B 伺服器上根本沒有這個 Session 檔案,於是使用者就被強制登出了。為了解決這個問題,我們得搞個 Session 共享機制,例如用一個獨立的 Redis 伺服器來儲存所有 Session,這無疑增加了架構的複雜度和維護成本。每次看到為了 Session 同步搞得焦頭爛額的專案,我頭都痛了。
  • 跨域與跨平台限制: Session ID 通常依賴 Cookie。如果你的前端(例如 Vue/React SPA)和後端 API 部署在不同網域,就會遇到惱人的 CORS (跨來源資源共用) 問題。更重要的是,非瀏覽器的客戶端(如手機 App、桌面應用程式)對 Cookie 的支援並不完善,管理起來非常麻煩。
  • 耦合性高: Session 機制讓客戶端與伺服器緊密地綁在一起,伺服器需要花費資源去維護大量的 Session 狀態,這在追求輕量化的微服務架構中是個累贅。

JWT:一把通往無狀態世界的鑰匙

所謂「無狀態 (Stateless)」,指的是伺服器不需要儲存任何關於客戶端的狀態資訊。每一次從客戶端來的請求,都包含了所有必要的資訊,讓伺服器能夠獨立處理這次請求,不需要去翻找之前的紀錄。

JWT 正是實現無狀態認證的完美工具。它將使用者的認證資訊加密後產生一個 Token 字串,交給客戶端保管。客戶端之後的每次請求,只需要在 HTTP Header 中附上這個 Token,伺服器就能驗證 Token 的真偽並解析出使用者資訊,完全不需要在自己家裡(伺服器端)存放任何 Session 資料。這帶來的好處顯而易見:

  • 絕佳的擴展性: 因為伺服器不需儲存狀態,你可以輕易地增加或減少伺服器數量,任何一台伺服器都能處理任何一個帶有有效 Token 的請求。
  • 靈活的跨平台支援: JWT 不依賴 Cookie,可以放在 Header、URL 參數或 POST Body 中,對網頁、手機 App、甚至物聯網裝置都非常友善。
  • 解耦合: 認證邏輯被封裝在 Token 中,後端伺服器可以更專注於業務邏輯的處理。

深入淺出:JWT (JSON Web Token) 到底是什麼?

講了這麼多好處,JWT 到底是什麼神奇的東西?其實它就是一個經過特定格式編碼的字串,長得像這樣:xxxxx.yyyyy.zzzzz。它由三個部分組成,並用點 `.` 隔開。

JWT 的三位一體結構:Header, Payload, Signature

讓我們來解剖這個 Token 字串:

  • Header (標頭): 這裡存放了關於這個 Token 的元數據,主要有兩部分:Token 的類型(`typ`),也就是 JWT;以及所使用的簽名演算法(`alg`),例如 `HS256` (HMAC SHA-256)。這部分會被 Base64Url 編碼成 Token 的第一部分。
    {
      "alg": "HS256",
      "typ": "JWT"
    }
  • Payload (酬載): 這裡存放了實際要傳遞的資料,也就是所謂的「聲明 (Claims)」。Claims 分為三種:
    • Registered claims: 官方建議的一些預定義聲明,例如 `iss` (簽發者)、`exp` (過期時間)、`sub` (主題)、`aud` (受眾) 等。這些不是強制性的,但使用它們可以增加 Token 的互通性。
    • Public claims: 自定義的公開聲明,為避免衝突,名稱應在 IANA JSON Web Token Registry 中註冊或加上命名空間。
    • Private claims: 自定義的私有聲明,由通訊雙方自行約定。我們最常用到的就是這個,例如放入 `user_id`。

    切記!切記!切記!Payload 部分只是經過 Base64Url 編碼,任何人都可以輕易解碼回來看。所以,千萬不要在 Payload 中存放任何敏感資訊,例如使用者的密碼!

    {
      "sub": "1234567890",
      "name": "John Doe",
      "admin": true,
      "iat": 1516239022,
      "exp": 1675868400,
      "user_id": 42
    }

  • Signature (簽名): 這是 JWT 最關鍵的部分,用來驗證 Token 的完整性與來源的真實性。它的產生方式是:
    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      your-256-bit-secret
    )

    伺服器會用一個絕對不能外洩的密鑰 (Secret) 來對 Header 和 Payload 進行簽名。當伺服器收到 Token 時,會用同樣的方式再算一次簽名,如果跟 Token 裡的簽名一模一樣,就表示這個 Token 是由我方簽發的,而且中途沒有被任何人篡改過。

Laravel + JWT 實戰:從零到一打造安全的 API 端點

好了,理論課上完了,該來動手寫點 Code 了,不然手會癢。我們將使用 Laravel 生態系中最受歡迎的 JWT 套件 `tymon/jwt-auth` 來完成這次的實戰。

步驟一:安裝與設定 `tymon/jwt-auth` 套件

首先,打開你的終端機,進入 Laravel 專案目錄,執行 Composer 指令來安裝套件:

composer require tymon/jwt-auth

安裝完成後,發佈套件的設定檔,這樣我們才能進行客製化設定:

php artisan vendor:publish --provider="Tymon\JWTAuth\Providers\LaravelServiceProvider"

接著,產生一個 JWT 簽名用的密鑰。這個指令會在你的 `.env` 檔案中新增一個 `JWT_SECRET` 變數。這個密鑰非常重要,絕對不能外洩!

php artisan jwt:secret

步驟二:設定 User Model

我們需要讓 User Model 知道如何跟 JWT 互動。打開 `app/Models/User.php`,讓它實作 `Tymon\JWTAuth\Contracts\JWTSubject` 這個介面,並加上兩個必要的方法:

<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Tymon\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements 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 (通常就是主鍵),它將被存放在 JWT Payload 的 `sub` 聲明中。`getJWTCustomClaims()` 則可以讓你加入額外的自訂聲明,例如使用者角色等。

步驟三:設定 Auth Guard

接下來,我們要告訴 Laravel,當處理 API 相關的請求時,要使用 JWT 這個「警衛 (Guard)」來做認證。打開 `config/auth.php`,修改 `guards` 的設定:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],

    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

我們把 `guards` 陣列中的 `api` driver 從預設的 `token` 或 `sanctum` 改成 `jwt`。

步驟四:建立認證路由與 Controller

現在來定義我們的 API 路由。打開 `routes/api.php`:

<?php

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 artisan make:controller API/AuthController

打開剛剛建立的 `app/Http/Controllers/API/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
{
    /**
     * Create a new AuthController instance.
     *
     * @return void
     */
    public function __construct()
    {
        // 除了 login 以外,其他 function 都需要經過 auth:api 中介層
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * Get a JWT via given credentials.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function login(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (! $token = auth('api')->attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

        return $this->respondWithToken($token);
    }

    /**
     * Get the authenticated User.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function me()
    {
        return response()->json(auth('api')->user());
    }

    /**
     * Log the user out (Invalidate the token).
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function logout()
    {
        auth('api')->logout();

        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * Refresh a token.
     *
     * @return \Illuminate\Http\JsonResponse
     */
    public function refresh()
    {
        return $this->respondWithToken(auth('api')->refresh());
    }

    /**
     * Get the token array structure.
     *
     * @param  string $token
     *
     * @return \Illuminate\Http\JsonResponse
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => auth('api')->factory()->getTTL() * 60
        ]);
    }
}

到這裡,一個基本的 JWT 認證系統就完成了!你可以用 Postman 或其他 API 測試工具來測試 `POST /api/auth/login`,如果帳號密碼正確,你就會收到一個包含 `access_token` 的 JSON 回應。接著,你可以帶著這個 Token(放在 `Authorization` Header,格式為 `Bearer {your_token}`)去請求 `/api/auth/me`,就能成功獲取到使用者資訊了。

JWT 的安全考量與最佳實踐

工具是死的,人是活的。光會用套件還不夠,一個稱職的工程師還必須了解潛在的安全風險與最佳實踐。

Token 該存在哪裡?LocalStorage vs. Cookies

這是一個經典的辯論題。將 Token 存在瀏覽器的 `LocalStorage` 非常方便,但它最大的風險是容易受到 XSS (跨站腳本) 攻擊。如果你的網站有 XSS 漏洞,攻擊者的腳本就可以輕易讀取到 `LocalStorage` 中的 Token 並盜用。相較之下,將 Token 存在設定為 `HttpOnly` 的 Cookie 中會更安全,因為 `HttpOnly` 屬性可以防止 JavaScript 讀取 Cookie。但 Cookie 也有 CSRF (跨站請求偽造) 的風險,不過這可以透過設定 Cookie 的 `SameSite` 屬性來有效防範。

Token 的生命週期管理

JWT 一旦簽發,在過期之前都是有效的。這意味著如果 Token 洩漏,攻擊者就能一直使用它直到過期。因此,最佳實踐是:

  • 使用短期的 Access Token: 將 `access_token` 的有效期設得短一些,例如 15 分鐘或 1 小時 (可以在 `config/jwt.php` 中的 `ttl` 設定)。
  • 搭配長期的 Refresh Token: 當 Access Token 過期後,客戶端可以使用一個有效期較長(例如數天或數週)的 `refresh_token` 去換取一個新的 Access Token,這樣使用者就不需要一直重新登入,兼顧了安全性與使用者體驗。`tymon/jwt-auth` 套件本身就支援 Token 的刷新機制。

黑名單機制:如何強制 Token 失效?

因為 JWT 的無狀態特性,伺服器本身無法主動讓一個未過期的 Token 失效。如果使用者登出、修改密碼,或是你偵測到某個 Token 被盜用,該怎麼辦?答案是引入「黑名單 (Blacklist)」機制。當使用者登出時,可以將該 Token 加入到一個黑名單中(通常用 Redis 或 Memcached 實現),並設定該黑名單的過期時間等於 Token 的剩餘有效期。之後每次驗證 Token 時,除了檢查簽名和過期時間,還要多一步檢查它是否在黑名單裡。`tymon/jwt-auth` 也內建了這個功能,只需要在設定檔中啟用即可。

結論:為你的 Laravel API 穿上 JWT 防彈背心

恭喜你!跟著 Eric 的腳步,我們不僅理解了無狀態認證的核心概念,也親手在 Laravel 中實現了一套完整的 JWT 認證流程。從傳統 Session 的困境,到 JWT 的結構解析,再到結合 `tymon/jwt-auth` 的實戰演練與安全最佳實踐,你已經掌握了現代 API 開發中不可或缺的一項關鍵技能。

請記住,技術的選擇從來都不是盲從流行,而是基於對場景和原理的深刻理解。JWT 為我們解決了傳統 Session 在擴展性和跨平台支援上的諸多痛點,讓我們能更優雅、更高效地建構現代化的應用程式。當然,安全是一個持續對抗的過程,沒有一勞永逸的銀彈。保持好奇心,持續學習,才能為你的專案打造出真正堅固的防線。

希望這篇文章對你有幫助。如果你在實作過程中遇到任何問題,或是對於 Laravel、API 開發有更深入的需求,都歡迎與我們交流。

延伸閱讀

在數位轉型的浪潮中,一個安全、穩定且高效的 API 是企業成功的基石。浪花科技擁有豐富的 Laravel 與 WordPress 系統開發經驗,擅長打造企業級的後端系統與 API 服務。如果你正在尋找一個專業的技術夥伴來協助你規劃或重構系統架構,歡迎點擊這裡,填寫表單與我們聯繫,讓我們一起為你的專案打造穩固的技術核心!

常見問題 (FAQ)

Q1: JWT 和傳統 Session 最大的不同是什麼?

最大的不同在於「狀態儲存」。傳統 Session 將使用者狀態儲存在伺服器端,是「有狀態 (Stateful)」的;而 JWT 將使用者資訊加密後交由客戶端儲存,伺服器本身不保存任何狀態,是「無狀態 (Stateless)」的。這個根本性的差異使得 JWT 在系統擴展性、跨平台支援方面具有巨大優勢。

Q2: JWT 存在瀏覽器的 LocalStorage 安全嗎?

這不是最安全的做法。存在 LocalStorage 的主要風險是 XSS (跨站腳本攻擊),惡意腳本可以輕易讀取並盜用你的 Token。對於網頁應用程式,更推薦的做法是將 Token 存放在設定了 `HttpOnly`、`Secure` 和 `SameSite` 屬性的 Cookie 中,以大幅提高安全性。

Q3: 如果 JWT 在過期前被盜用了怎麼辦?

這是一個很好的問題,也是 JWT 的一個挑戰。主要有三道防線:1. 縮短 Access Token 的有效期(例如15分鐘),即使被盜用,危害時間也有限。2. 搭配 Refresh Token 機制,在不影響使用者體驗的前提下頻繁更換 Access Token。3. 建立 Token 黑名單機制,一旦偵測到異常或使用者登出,就立刻將該 Token 加入黑名單,讓它立即失效。

Q4: 除了 `tymon/jwt-auth`,Laravel 還有其他 API 認證的選擇嗎?

當然有。Laravel 官方就提供了兩個很棒的套件:Laravel Sanctum 和 Laravel Passport。Sanctum 特別適合用於 SPA (單頁應用程式)、手機 App 等第一方客戶端的認證,它使用更簡單的 API Token 機制。Passport 則是一個完整的 OAuth2 伺服器實現,適用於需要提供給第三方應用程式使用的 API,功能更為強大和複雜。選擇哪一個取決於你的具體應用場景。

 
立即諮詢,索取免費1年網站保固