API 沒鎖門等於裸奔!2026 Laravel JWT 實戰:從 Refresh Token 輪替到黑名單機制的資安防護網

2026/02/20 | API 串接與自動化, Laravel技術分享, 網站安全與防護

API 沒鎖門等於裸奔!2026 Laravel JWT 實戰:從 Refresh Token 輪替到黑名單機制的資安防護網

嗨大家好,我是浪花科技的資深工程師 Eric。又是喝著黑咖啡、盯著 Terminal 發呆的午後。最近幫客戶做系統健檢,打開他們的 API Log 一看,真的差點沒把剛喝進去的咖啡噴在螢幕上。為什麼?因為他們的 API 根本就是「裸奔」狀態,雖然有做 Token 驗證,但那個 Token 的效期居然設了「永久」!沒錯,就是一旦發出去,除非改資料庫,否則拿到 Token 的人(或駭客)可以一輩子自由進出你的系統。

在 2026 年的今天,AI 攻擊機器人滿街跑,這種設定跟把家裡鑰匙掛在大門口有什麼兩樣?

很多開發者聽到 JWT (JSON Web Token) 都覺得:「喔,我不就是裝個套件、產生一串亂碼,然後前端放在 Header 帶過來就好了嗎?」大錯特錯。真正的 JWT 機制,核心在於「無狀態 (Stateless)」的特性以及「Refresh Token (刷新權杖) 的輪替機制」。今天這篇文章,Eric 要帶大家深入 Laravel API 開發的核心,手把手實作一套真正符合 2026 年資安標準的 JWT 認證系統。

為什麼 2026 年我們還在談 JWT?

你可能會問,Laravel 不是有 Sanctum 和 Passport 嗎?為什麼還要自找麻煩用 JWT?

確實,對於簡單的 SPA (Single Page Application) 或同網域的應用,Sanctum 的 Cookie-based 驗證非常方便。但在以下情境,JWT 依然是霸主:

  • 跨網域微服務 (Microservices): 當你的認證中心 (Auth Service) 和資源伺服器 (Resource Server) 是分開的,JWT 的自包含 (Self-contained) 特性讓資源伺服器不需要查資料庫就能驗證身份。
  • 高併發 Mobile App: 手機端不需要維護複雜的 Cookie 狀態,Token 存取更直觀。
  • Server-to-Server 通訊: 機器與機器之間的 API 溝通,JWT 是最通用的標準。

實戰開始:環境準備與套件安裝

假設你已經安裝好了 Laravel 12 或更高版本(沒錯,我們活在 2026 年)。我們依然推薦使用社群維護最穩定的 php-open-source-saver/jwt-auth 套件。

1. 安裝套件

composer require php-open-source-saver/jwt-auth

2. 發布設定檔

這一步很重要,很多人裝完就直接用,結果連密鑰都沒換。請執行:

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

3. 產生 JWT Secret

這把鑰匙是簽署 Token 的核心,千萬不能外流。這指令會幫你在 .env 檔案中寫入 JWT_SECRET

php artisan jwt:secret

核心觀念:Access Token 與 Refresh Token 的黃金比例

這是我最常碎念新進工程師的地方。為了安全,我們必須將 Token 分為兩種:

  • Access Token (存取權杖): 效期極短(例如 15-60 分鐘)。用來存取 API 資源。因為效期短,就算被劫持,駭客能用的時間也很有限。
  • Refresh Token (刷新權杖): 效期較長(例如 2 週)。唯一用途就是用來換取新的 Access Token。

config/jwt.php 中,我們需要設定 TTL (Time To Live):


// 單位是分鐘,這裡設定 Access Token 為 60 分鐘
'ttl' => env('JWT_TTL', 60),

// Refresh TTL 設定為 20160 分鐘 (兩週)
'refresh_ttl' => env('JWT_REFRESH_TTL', 20160),

程式碼實作:User Model 設定

讓你的 User Model 實作 JWTSubject 介面。這告訴 JWT 套件:「嘿,這個 Model 是要用來產生 Token 的。」


<?php

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;
use PHPOpenSourceSaver\JWTAuth\Contracts\JWTSubject;

class User extends Authenticatable implements JWTSubject
{
    // ... 其他程式碼 ...

    /**
     * 取得會被儲存在 JWT 中的識別字 (通常是 ID)
     */
    public function getJWTIdentifier()
    {
        return $this->getKey();
    }

    /**
     * 回傳自訂的 Claims (想要放在 Token 裡的額外資訊)
     */
    public function getJWTCustomClaims()
    {
        return [
            'role' => $this->role, // 比如把角色放進去,前端解析 Token 就能知道權限
            'iss'  => 'RoamerTech_Auth_Server' // 簽發者
        ];
    }
}

Controller 實戰:登入與 Token 簽發

這裡我們要寫一個 AuthController。注意看 respondWithToken 這個方法,這是 Eric 的習慣寫法,統一回傳格式,方便前端工程師接資料。


<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Controllers\Controller;

class AuthController extends Controller
{
    public function __construct()
    {
        // 除了 login,其他方法都需要驗證
        $this->middleware('auth:api', ['except' => ['login']]);
    }

    /**
     * 登入並取得 Token
     */
    public function login(Request $request)
    {
        $credentials = $request->only('email', 'password');

        // 嘗試登入,如果失敗回傳 401
        if (! $token = Auth::attempt($credentials)) {
            return response()->json(['error' => 'Unauthorized'], 401);
        }

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

    /**
     * 取得當前登入的使用者資訊
     */
    public function me()
    {
        return response()->json(Auth::user());
    }

    /**
     * 登出 (讓 Token 失效)
     */
    public function logout()
    {
        Auth::logout();
        return response()->json(['message' => 'Successfully logged out']);
    }

    /**
     * 刷新 Token (重點!)
     */
    public function refresh()
    {
        // 這裡會將舊的 Token 加入黑名單,並回傳一個全新的 Token
        return $this->respondWithToken(Auth::refresh());
    }

    /**
     * 統一的回傳格式構造器
     */
    protected function respondWithToken($token)
    {
        return response()->json([
            'access_token' => $token,
            'token_type' => 'bearer',
            'expires_in' => Auth::factory()->getTTL() * 60 // 回傳秒數
        ]);
    }
}

資安關鍵:Token 黑名單 (Blacklist) 機制

這就是「無狀態」驗證最讓人頭痛的地方:如果使用者的手機被偷了,或者我要強制踢某人下線,但我發出去的 Token 還沒過期,怎麼辦?

這時候就需要「黑名單」機制。jwt-auth 套件預設支援這個功能。當使用者呼叫 logoutrefresh 時,舊的 Token 雖然還沒過期,但會被存入 Cache (通常是 Redis) 標記為「已無效」。

確保你的 .env 設定正確:

JWT_BLACKLIST_ENABLED=true
JWT_BLACKLIST_GRACE_PERIOD=30 # 寬限期 30 秒,避免併發請求時舊 Token 瞬間失效導致錯誤

Eric 建議一定要裝 Redis 來處理黑名單。因為如果是用資料庫或檔案系統,每個 API Request 都要去查表,效能會掉得比股市崩盤還快。

2026 進階防禦:前端如何安全儲存?

寫後端的不能只顧自己爽,也要教前端怎麼存。在 2026 年,將 Token 存在 localStorage 已經被視為高風險行為,因為只要網站有 XSS 漏洞,Token 馬上就會被偷走。

最佳實踐建議:

  1. 記憶體儲存 (In-Memory): Access Token 只存在 JS 變數中,頁面刷新就消失。
  2. HttpOnly Cookie 儲存 Refresh Token: 將 Refresh Token 放在 HttpOnlySecureSameSite=Strict 的 Cookie 中。

這樣做的好處是,JS 讀不到 Cookie (防 XSS),而 Access Token 存在記憶體中,駭客就算攻擊進來也拿不到持久的登入憑證。

常見的 API 認證錯誤 (不要再犯了!)

  • 錯誤 1:Token 效期設太長。 不要為了省去 Refresh 的工,把 Access Token 設為一個月。
  • 錯誤 2:不驗證簽發者 (iss)。 如果你的公司有多個服務,務必檢查 iss 欄位,避免 A 服務的 Token 被拿來存取 B 服務。
  • 錯誤 3:將敏感資料塞進 Payload。 JWT 只是 Base64 編碼,不是加密!任何人拿到 Token 都能解碼看到內容。千萬不要把密碼、身分證字號放在裡面。

相關閱讀

要打造一個企業級的 Laravel 系統,單靠 JWT 是不夠的。以下這幾篇 Eric 精選的文章,建議一併服用:

常見問題 (FAQ)

Q1: JWT 與 Sanctum 到底該選誰?

如果是 Laravel 作為後端,Vue/React 作為前端的 SPA 架構,且都在同一個主網域下,選 Sanctum (Cookie-based) 最安全簡單。如果是開發純 API 供 Android/iOS 或第三方廠商介接,選 JWT。

Q2: Refresh Token 過期了怎麼辦?

這代表使用者的登入狀態已經太久沒活動,或者超過了強制登出時間。這時候前端應該導向登入頁面,請使用者重新輸入帳號密碼登入。

Q3: 為什麼我的 JWT 黑名單功能沒效?

請檢查 .env 中的 JWT_BLACKLIST_ENABLED 是否為 true,並確認你的 Cache Driver 是否設定正確(建議使用 Redis)。另外,檢查 storage 目錄的權限是否足夠(如果是用 file driver)。

Q4: Token 被偷了怎麼辦?

由於 JWT 是無狀態的,一旦 Access Token 被偷,駭客在效期內都能使用。這就是為什麼 Access Token 效期要短。若發現異常,可以透過更改使用者的 jwt-auth secret (如果邏輯支援) 或在黑名單機制中強制讓該 User ID 的所有 Token 失效。

資安這條路是沒有終點的,JWT 只是基本功。如果你在實作 Laravel API 認證、或是系統架構優化上遇到瓶頸,不知道該怎麼設計最安全、最高效的驗證流程,別自己悶著頭寫 Code 了。

有時候,一個資深工程師的建議,可以幫你省下好幾週的 Debug 時間。

歡迎隨時聯繫浪花科技,讓我們來幫你的系統做個深度健檢!