跳至主要內容
技術

Laravel 認證與授權:用 Starter Kit 十分鐘搞定會員系統

Laravel 認證與授權:用 Starter Kit 十分鐘搞定會員系統
PHP/Laravel 完全指南 第 6 / 15 篇

本篇是「PHP/Laravel 完全指南」系列的第 6 / 15 篇。你可以從系列總覽開始閱讀,也可以直接接著看本文。

每一個有使用者的應用程式,都逃不過這兩件事:認證(Authentication)和授權(Authorization)。認證是「你是誰」,授權是「你能做什麼」。聽起來簡單,但自己從零刻一套會員系統——處理密碼雜湊、Session 管理、忘記密碼信件、Email 驗證、CSRF 防護——光想就頭痛。更何況,這些東西稍有閃失就是資安漏洞。

好消息是,Laravel 根本不讓你自己造這個輪子。Laravel 12 推出了全新的官方 Starter Kit,取代了舊版的 Breeze 和 Jetstream。一行指令就幫你搞定註冊、登入、密碼重設、Email 驗證的完整流程,連前端 UI 都幫你生好了。你可以選擇 React、Vue、Livewire(推出時的三種官方選項,2026 年 2 月再補上 Svelte)作為前端堆疊——我們選 Livewire,因為整本書都在 PHP 生態系裡。

在「揪好買」團購平台裡,我們有兩種角色:開團主和跟團者。開團主可以建立團購、設定截止時間、管理訂單;跟團者只能瀏覽和加入。這一章,我們要用 Starter Kit 搞定會員系統,再用 Policy 把「只有開團主能建立團購」這條規則寫得乾淨俐落。

認證 vs 授權:先搞清楚這兩件事

這兩個詞經常被混用,但它們是完全不同的概念:

認證(Authentication)授權(Authorization)
問題你是誰?你能做什麼?
時機登入時每次操作時
失敗結果401 Unauthorized403 Forbidden
Laravel 工具Guard、Session、TokenGate、Policy
類比門口刷員工證你的員工證能進哪些房間

認證通常只需要做一次(用套件),授權則貫穿整個應用程式(用你的業務邏輯)。這一章前半講認證,後半講授權。

跨框架對照

概念LaravelExpress (Node.js)Django (Python)
認證套件Starter KitPassport.jsdjango.contrib.auth
Session內建express-session內建
密碼雜湊Hash::make()bcryptmake_password()
授權Gate / Policy自己寫 middlewarePermissions / Decorators
Token 認證SanctumjsonwebtokenDRF TokenAuth

Laravel 12 Starter Kit:十分鐘擁有完整會員系統

什麼是 Starter Kit?

Laravel 12 的 Starter Kit 是官方維護的應用程式啟動模板。它直接把認證相關的 Controller、View、Route 全部放進你的專案裡——不是 Composer 套件,而是真正的程式碼。你可以看到每一行、改每一處。

歷史脈絡: Laravel 11 以前用的是 Breeze(輕量)和 Jetstream(重量級,含 Team 管理)。Laravel 12 把它們統一成了新的 Starter Kit 系列,推出時提供 React、Vue、Livewire 三種前端選項,2026 年 2 月再加入官方 Svelte + Inertia kit,目前共四種。如果你看到舊教學提到 Breeze,概念是一樣的,只是安裝方式不同。

安裝 Livewire Starter Kit

如果你在第二章建專案時選了 “No starter kit”,現在可以重新建一個:

laravel new jiu-hao-mai

在互動式選單中選擇:

 ┌ Would you like to install a starter kit? ──────┐
 │ › Livewire                                       │
 └──────────────────────────────────────────────────┘

或者,如果你想在現有專案上操作,最簡單的做法是用 laravel new 建一個新專案,再把認證相關的檔案複製過來。

安裝完你得到了什麼?

Starter Kit 幫你生成的東西:

app/
└── Livewire/Auth/               # 認證邏輯放在 Livewire 元件,而非傳統 Controller
    ├── Login.php
    ├── Register.php
    ├── ResetPassword.php
    └── VerifyEmail.php
resources/views/
├── livewire/auth/               # 認證頁面模板
│   ├── login.blade.php
│   ├── register.blade.php
│   └── ...
├── components/layouts/
│   └── app.blade.php            # 應用程式 Layout
routes/
└── auth.php                     # 認證路由(Volt::route 風格)

注意: Laravel 12 Livewire Starter Kit 不會產生傳統的 app/Http/Controllers/Auth/ 目錄。認證邏輯改用 Livewire 元件(app/Livewire/Auth/)處理,不再是 LoginController、RegisterController 等傳統 Controller。

所有程式碼都在你的專案裡——不是躲在 vendor/ 裡的黑盒子。

註冊、登入、忘記密碼:開箱即用

安裝完 Starter Kit 後,這些路由就自動可用了:

路由功能
GET /register註冊頁面
POST /register處理註冊
GET /login登入頁面
POST /login處理登入
POST /logout登出
GET /forgot-password忘記密碼頁面
POST /forgot-password寄送重設密碼信
GET /reset-password/{token}重設密碼頁面
POST /reset-password處理密碼重設
php artisan serve
# 打開 http://localhost:8000/register

你會看到一個設計好的註冊頁面。填入資料、送出,帳號就建好了。登入、登出、忘記密碼——全部能用。

在 Controller 裡取得當前使用者

use Illuminate\Http\Request;

class DashboardController extends Controller
{
    public function index(Request $request)
    {
        // 方法一:從 Request 物件取
        $user = $request->user();

        // 方法二:用 Auth Facade
        $user = auth()->user();

        // 方法三:取得使用者 ID
        $userId = auth()->id();

        // 檢查是否已登入
        if (auth()->check()) {
            // 已登入
        }

        return view('dashboard', compact('user'));
    }
}

保護路由(要求登入才能存取)

// routes/web.php

// 方法一:單一路由
Route::get('/dashboard', [DashboardController::class, 'index'])
    ->middleware('auth');

// 方法二:路由群組(常用)
Route::middleware('auth')->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/my-groups', [GroupBuyController::class, 'myGroups']);
    Route::post('/group-buys', [GroupBuyController::class, 'store']);
});

// 方法三:只給「未登入」使用者(如登入頁)
Route::get('/login', [LoginController::class, 'show'])
    ->middleware('guest');

auth middleware 的邏輯很簡單:使用者沒登入 → 重新導向到 /login。就這樣。

密碼雜湊

Laravel 自動用 bcrypt 雜湊密碼,你永遠不會在資料庫裡看到明文密碼:

use Illuminate\Support\Facades\Hash;

// 建立雜湊(註冊時用)
$hashed = Hash::make('my-password');
// $2y$12$eUxcJq1...(60 字元雜湊字串)

// 驗證密碼(登入時用)
if (Hash::check('my-password', $hashed)) {
    // 密碼正確
}

重要: 永遠不要自己寫登入驗證邏輯。Starter Kit 已經幫你處理好了密碼雜湊、防暴力破解(rate limiting)、CSRF 防護等所有安全細節。

Email 驗證:確保使用者是真人

Email 驗證是「註冊後寄一封確認信,使用者點連結才算驗證完成」的功能。

啟用 Email 驗證

讓 User Model 實作 MustVerifyEmail 介面:

// app/Models/User.php
use Illuminate\Contracts\Auth\MustVerifyEmail;

class User extends Authenticatable implements MustVerifyEmail
{
    // ...
}

就這一行,Laravel 會自動:

  • 在註冊後寄出驗證信
  • 提供 /verify-email 頁面
  • 驗證連結有簽名保護(防偽造)

限制未驗證使用者

Route::middleware(['auth', 'verified'])->group(function () {
    // 這裡的路由只有驗證過 email 的使用者才能進
    Route::post('/group-buys', [GroupBuyController::class, 'store']);
});

開發環境的 Email

開發階段不用真的寄信。.env 預設用 log driver:

MAIL_MAILER=log

所有「寄出」的信件都會記錄在 storage/logs/laravel.log 裡,你可以從 log 裡找到驗證連結。

Gate 與 Policy:誰可以做什麼事

認證搞定了(你是誰),現在來處理授權(你能做什麼)。

Gate:簡單的授權檢查

Gate 是最基本的授權方式——定義一個 closure,回傳 truefalse

// app/Providers/AppServiceProvider.php 的 boot() 裡
use Illuminate\Support\Facades\Gate;
use App\Models\GroupBuy;
use App\Models\User;

public function boot(): void
{
    Gate::define('create-group-buy', function (User $user) {
        return $user->is_organizer;
    });

    Gate::define('update-group-buy', function (User $user, GroupBuy $groupBuy) {
        return $user->id === $groupBuy->user_id;
    });
}

使用方式:

// 在 Controller 裡
if (Gate::allows('create-group-buy')) {
    // 可以建立團購
}

if (Gate::denies('update-group-buy', $groupBuy)) {
    abort(403);
}

// 更簡潔:authorize(失敗自動丟 403)
Gate::authorize('create-group-buy');

// 在 Blade 裡
@can('create-group-buy')
    <a href="/group-buys/create">+ 我要開團</a>
@endcan

@cannot('update-group-buy', $groupBuy)
    <p>你沒有權限編輯這個團購</p>
@endcannot

Policy:更有組織的授權

Gate 適合一兩條簡單規則。當授權邏輯變多,Policy 把同一個 Model 的授權規則集中在一個 class 裡:

php artisan make:policy GroupBuyPolicy --model=GroupBuy
<?php

namespace App\Policies;

use App\Models\GroupBuy;
use App\Models\User;

class GroupBuyPolicy
{
    /**
     * 誰可以看列表?所有人。
     */
    public function viewAny(?User $user): bool
    {
        return true;  // ?User 表示未登入也可以
    }

    /**
     * 誰可以看單一團購?所有人。
     */
    public function view(?User $user, GroupBuy $groupBuy): bool
    {
        return true;
    }

    /**
     * 誰可以建立團購?已驗證 email 的使用者。
     */
    public function create(User $user): bool
    {
        return $user->hasVerifiedEmail();
    }

    /**
     * 誰可以更新團購?只有開團者本人。
     */
    public function update(User $user, GroupBuy $groupBuy): bool
    {
        return $user->id === $groupBuy->user_id;
    }

    /**
     * 誰可以刪除團購?
     * 只有開團者本人,而且團購還沒有人加入。
     */
    public function delete(User $user, GroupBuy $groupBuy): bool
    {
        return $user->id === $groupBuy->user_id
            && $groupBuy->participants()->count() === 0;
    }
}

在 Controller 裡使用 Policy

class GroupBuyController extends Controller
{
    public function index()
    {
        // viewAny 不需要特定 model 實例
        $this->authorize('viewAny', GroupBuy::class);

        return view('group-buys.index');
    }

    public function create()
    {
        $this->authorize('create', GroupBuy::class);

        return view('group-buys.create');
    }

    public function store(Request $request)
    {
        $this->authorize('create', GroupBuy::class);

        // 建立團購...
    }

    public function edit(GroupBuy $groupBuy)
    {
        $this->authorize('update', $groupBuy);

        return view('group-buys.edit', compact('groupBuy'));
    }

    public function destroy(GroupBuy $groupBuy)
    {
        $this->authorize('delete', $groupBuy);

        $groupBuy->delete();

        return redirect('/group-buys')->with('success', '團購已刪除');
    }
}

$this->authorize() 會自動找到 GroupBuyPolicy(靠命名慣例),檢查對應的方法。失敗就丟 403 Forbidden。

Policy 的自動發現: Laravel 會自動把 GroupBuy Model 對應到 GroupBuyPolicy。你不需要手動註冊——命名慣例搞定一切。

在 Blade 裡使用 Policy

@can('create', App\Models\GroupBuy::class)
<a href="/group-buys/create" class="btn-primary">+ 我要開團</a>
@endcan

@can('update', $groupBuy)
<a href="/group-buys/{{ $groupBuy->id }}/edit">編輯</a>
@endcan

@can('delete', $groupBuy)
<form method="POST" action="/group-buys/{{ $groupBuy->id }}">
  @csrf
  @method('DELETE')
  <button type="submit" class="text-red-600">刪除</button>
</form>
@endcan

Role-based 權限設計模式

揪好買需要區分「開團主」和「跟團者」。最簡單的做法是在 users 表加一個 role 欄位:

方案一:簡單的 role 欄位

php artisan make:migration add_role_to_users_table
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('role')->default('member');  // member, organizer, admin
    });
}

在 User Model 加上 helper 方法:

// app/Models/User.php

// 用 Enum 定義角色(型別安全)
enum UserRole: string
{
    case Member = 'member';
    case Organizer = 'organizer';
    case Admin = 'admin';
}

class User extends Authenticatable implements MustVerifyEmail
{
    protected function casts(): array
    {
        return [
            'email_verified_at' => 'datetime',
            'password' => 'hashed',
            'role' => UserRole::class,
        ];
    }

    public function isOrganizer(): bool
    {
        return $this->role === UserRole::Organizer
            || $this->role === UserRole::Admin;
    }

    public function isAdmin(): bool
    {
        return $this->role === UserRole::Admin;
    }
}

然後在 Policy 裡使用:

public function create(User $user): bool
{
    return $user->isOrganizer() && $user->hasVerifiedEmail();
}

方案二:Spatie Permission 套件(多角色/多權限)

如果需要更複雜的權限系統(一個使用者可以有多個角色、角色可以有多個權限),推薦用 spatie/laravel-permission

composer require spatie/laravel-permission
php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
php artisan migrate
// 指派角色
$user->assignRole('organizer');

// 檢查角色
$user->hasRole('organizer');

// 指派權限
$user->givePermissionTo('create group buys');

// 在 Policy 裡用
public function create(User $user): bool
{
    return $user->can('create group buys');
}

揪好買用哪個? 我們的需求很簡單(member / organizer / admin 三種角色),方案一的 role 欄位就夠了。除非你的專案有「一個使用者同時是多個角色」或「權限需要動態新增/移除」的需求,才需要 Spatie Permission。YAGNI——You Ain’t Gonna Need It。

Guard:多重認證系統

Guard 決定的是「用什麼方式認證使用者」。預設的 web guard 用 Session + Cookie,api guard 用 Token。

大多數應用只用一個 Guard,你不需要動它。只有在這些情況下才需要自訂 Guard:

  • 前後台分離——Admin 和一般使用者用不同的 users 表
  • API 認證——用 Sanctum token 而不是 Session
  • 多租戶——不同租戶有不同的認證方式
// config/auth.php(通常不需要改)
'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
    // 安裝 Sanctum 後會多一個 sanctum guard
],

揪好買目前只需要 web guard,第十一章做 API 時才會用到 Sanctum

實作:揪好買的開團主與跟團者

讓我們把認證和授權整合進揪好買。

Step 1:加入 role 欄位

php artisan make:migration add_role_to_users_table
public function up(): void
{
    Schema::table('users', function (Blueprint $table) {
        $table->string('role')->default('member');
    });
}
php artisan migrate

Step 2:更新 User Model

app/Models/User.php 加入:

use App\Enums\UserRole;

// 在 $fillable 加入 'role'
protected $fillable = [
    'name', 'email', 'password', 'role',
];

protected function casts(): array
{
    return [
        'email_verified_at' => 'datetime',
        'password' => 'hashed',
        'role' => UserRole::class,
    ];
}

public function isOrganizer(): bool
{
    return $this->role === UserRole::Organizer
        || $this->role === UserRole::Admin;
}

public function isAdmin(): bool
{
    return $this->role === UserRole::Admin;
}

建立 Enum app/Enums/UserRole.php

<?php

namespace App\Enums;

enum UserRole: string
{
    case Member = 'member';
    case Organizer = 'organizer';
    case Admin = 'admin';

    public function label(): string
    {
        return match ($this) {
            self::Member => '一般會員',
            self::Organizer => '開團主',
            self::Admin => '管理員',
        };
    }
}

Step 3:建立 GroupBuyPolicy

php artisan make:policy GroupBuyPolicy --model=GroupBuy

把前面的 Policy 程式碼放進去(viewAny、view、create、update、delete)。

Step 4:保護路由

// routes/web.php
use App\Http\Controllers\GroupBuyController;

// 任何人都能看
Route::get('/group-buys', [GroupBuyController::class, 'index']);
Route::get('/group-buys/{groupBuy}', [GroupBuyController::class, 'show']);

// 需要登入 + Email 驗證
Route::middleware(['auth', 'verified'])->group(function () {
    Route::get('/group-buys/create', [GroupBuyController::class, 'create']);
    Route::post('/group-buys', [GroupBuyController::class, 'store']);
    Route::get('/group-buys/{groupBuy}/edit', [GroupBuyController::class, 'edit']);
    Route::put('/group-buys/{groupBuy}', [GroupBuyController::class, 'update']);
    Route::delete('/group-buys/{groupBuy}', [GroupBuyController::class, 'destroy']);
});

Step 5:在 View 裡顯示角色相關 UI

{{-- 導覽列 --}}
<nav>
  @auth
  <span>{{ auth()->user()->name }}{{ auth()->user()->role->label() }})</span>

  @can('create', App\Models\GroupBuy::class)
  <a href="/group-buys/create">+ 我要開團</a>
  @endcan
  @endauth
</nav>

{{-- 團購詳情頁 --}}
@can('update', $groupBuy)
<a href="/group-buys/{{ $groupBuy->id }}/edit" class="btn">編輯團購</a>
@endcan

@can('delete', $groupBuy)
<form method="POST" action="/group-buys/{{ $groupBuy->id }}">
  @csrf
  @method('DELETE')
  <button onclick="return confirm('確定要刪除?')" class="text-red-600">刪除</button>
</form>
@endcan

講真的,這一步最容易讓新手誤會:@can 只是把按鈕藏起來,它不是防線。 編輯按鈕看不到,不代表那個 endpoint 被擋住了。任何人打開 DevTools、或直接用 curl/Postman 對著 PUT /group-buys/5 送一發請求,照樣會打進你的 Controller。真正的防線在後端——所以每一個會改資料的動作(store / update / destroy)都得自己再 authorize() 一次。少了這層,就是教科書等級的 IDOR 越權漏洞:A 開的團,B 改網址裡的 id 就能刪。

所以 Step 4 在路由套 auth + verified 還不夠——那只擋「沒登入的人」,擋不了「登入了但不該動這筆資料的人」。把前面那個 GroupBuyControllerauthorize() 真的接上去,每個寫入動作都檢查一次:

class GroupBuyController extends Controller
{
    public function store(Request $request)
    {
        $this->authorize('create', GroupBuy::class);
        // 通過才建立團購...
    }

    public function update(Request $request, GroupBuy $groupBuy)
    {
        $this->authorize('update', $groupBuy);
        // 通過才更新...
    }

    public function destroy(GroupBuy $groupBuy)
    {
        $this->authorize('delete', $groupBuy);
        $groupBuy->delete();

        return redirect('/group-buys')->with('success', '團購已刪除');
    }
}

懶一點也可以走路由層,把檢查直接掛在路由上,效果一樣:Route::put('/group-buys/{groupBuy}', ...)->can('update', 'groupBuy');。重點不是用哪種寫法,是「Blade 藏按鈕」跟「後端擋請求」永遠要成對出現,缺一個都不算授權做完。

Step 6:Seeder 建立測試帳號

// database/seeders/DatabaseSeeder.php
public function run(): void
{
    // 建立管理員
    User::factory()->create([
        'name' => 'Admin',
        'email' => 'admin@jiuhaomai.tw',
        'role' => 'admin',
    ]);

    // 建立開團主
    User::factory()->count(3)->create([
        'role' => 'organizer',
    ]);

    // 建立一般會員
    User::factory()->count(10)->create([
        'role' => 'member',
    ]);

    // ...GroupBuy seeder
}
php artisan migrate:fresh --seed

現在你可以用 admin@jiuhaomai.tw 登入,看到所有管理功能;用一般會員登入,只能看和跟團。

小結:認證用套件,授權用 Policy

這一章我們走過了 Laravel 認證授權的完整流程:

認證(你是誰):

  • Laravel 12 Starter Kit(Livewire 版)——一行安裝,開箱即用
  • 註冊、登入、登出、忘記密碼、Email 驗證全自動
  • auth middleware 保護路由,auth()->user() 取得當前使用者
  • 密碼自動 bcrypt 雜湊,永遠不要自己處理密碼

授權(你能做什麼):

  • Gate——簡單的一次性授權檢查
  • Policy——依 Model 組織的授權類別,自動發現
  • $this->authorize() 在 Controller 裡使用,@can 在 Blade 裡使用
  • Role-based 權限——簡單需求用 role 欄位 + Enum,複雜需求用 Spatie Permission

揪好買進度:

  • ✅ User Model 加入 role 欄位(member / organizer / admin)
  • ✅ UserRole Enum 定義角色和中文標籤
  • ✅ GroupBuyPolicy 定義 CRUD 權限規則
  • ✅ 路由保護(auth + verified middleware)
  • ✅ Blade 裡用 @can 顯示/隱藏操作按鈕

下一章,我們要讓開團主真正地建立團購——表單驗證與檔案上傳。你會學到 Laravel 強大的 Validation 規則系統、Form Request、以及用 Storage facade 處理商品圖片上傳。

留言討論

esc
輸入關鍵字搜尋文章...
查看收藏 →