你的 API 只是「能動」還是「安全」?Laravel + JWT 深度實戰,打造駭客也搖頭的認證機制

2025/09/15 | Laravel技術分享

你的 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,我們需要做兩件事:

  1. 引用 Tymon\JWTAuth\Contracts\JWTSubject 這個 interface。
  2. 實作 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。melogoutrefresh 等方法,我們在建構子中加入了 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) 權限管理,還有很多可以深入探索的地方。但今天,你已經邁出了最重要的一步。

相關閱讀

如果你在開發 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 套件本身就有支援這個機制,可以深入研究一下它的設定。

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