> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 自訂驗證 Guard 的實作

> 理解 Laravel 驗證系統的內部結構，並從框架原始碼層面解說如何實作 Guard、StatefulGuard 介面以建立自訂驗證 Guard。

## Laravel 驗證系統的內部結構

### `Auth` Facade 與 `AuthManager`

`Auth` Facade 是 `Illuminate\Auth\AuthManager` 的代理。`AuthManager` 以 Driver 模式管理多個 Guard，並依照 `config/auth.php` 的設定產生並快取適當的 Guard 實例。

```php theme={null}
// Auth::guard('web') 的內部運作（AuthManager::guard() 的簡略版）
public function guard($name = null)
{
    $name = $name ?: $this->getDefaultDriver();

    return $this->guards[$name] ?? ($this->guards[$name] = $this->resolve($name));
}
```

`resolve()` 會從 `config/auth.php` 的 `guards` 陣列讀取 `driver` 鍵，並呼叫對應的 factory closure。內建的 `session`、`token` driver 也是以相同機制註冊的。

### `Guard` 介面與 `StatefulGuard` 介面的差異

Laravel 的驗證 Guard 至少須實作 `Illuminate\Contracts\Auth\Guard`。若需維持 session，則實作 `StatefulGuard`。

<Accordion title="Guard 介面（Illuminate\Contracts\Auth\Guard）">
  ```php theme={null}
  interface Guard
  {
      // 確認是否存在已驗證的使用者
      public function check();

      // 確認是否為 guest（未驗證）
      public function guest();

      // 回傳當前已驗證的使用者（未驗證時為 null）
      public function user();

      // 回傳當前已驗證使用者的 ID
      public function id();

      // 確認指定的憑證是否有效（不會執行登入）
      public function validate(array $credentials = []);

      // 確認是否已設定使用者（即使未登入亦可透過 setUser 注入）
      public function hasUser();

      // 手動設定已驗證的使用者
      public function setUser(Authenticatable $user);
  }
  ```
</Accordion>

<Accordion title="StatefulGuard 介面（Illuminate\Contracts\Auth\StatefulGuard）">
  `StatefulGuard` 繼承 `Guard`，並加入使用 session 或 cookie 維持登入狀態所需的方法。

  ```php theme={null}
  interface StatefulGuard extends Guard
  {
      // 驗證憑證後登入（可指定 remember 旗標）
      public function attempt(array $credentials = [], $remember = false);

      // 不保存 session，僅於一次請求中進行驗證
      public function once(array $credentials = []);

      // 直接以 User 實例登入
      public function login(Authenticatable $user, $remember = false);

      // 以 ID 登入
      public function loginUsingId($id, $remember = false);

      // 以 ID 進行一次請求的驗證
      public function onceUsingId($id);

      // 確認是否透過「記住登入」cookie 登入
      public function viaRemember();

      // 登出
      public function logout();
  }
  ```
</Accordion>

<Info>
  對於 API 驗證或自訂 Token 驗證等不需 session 的 Guard，僅需實作 `Guard` 即可。若如管理員登入等需要 session 時，則實作 `StatefulGuard`。
</Info>

## 自訂 Guard 的實作

### `GuardHelpers` trait

由於 `Guard` 介面的 `check()`、`guest()`、`id()`、`hasUser()` 幾乎皆為相同實作，Laravel 提供了 `Illuminate\Auth\GuardHelpers` trait。使用此 trait 後，必要實作的內容便可縮減至 `user()` 與 `validate()` 兩個方法。

```php theme={null}
// GuardHelpers 提供的預設實作（節錄）
trait GuardHelpers
{
    protected $user;

    public function check(): bool
    {
        return ! is_null($this->user());
    }

    public function guest(): bool
    {
        return ! $this->check();
    }

    public function id(): mixed
    {
        return $this->user()?->getAuthIdentifier();
    }

    public function hasUser(): bool
    {
        return ! is_null($this->user);
    }

    public function setUser(Authenticatable $user): static
    {
        $this->user = $user;
        return $this;
    }
}
```

### API Token 驗證 Guard 的實作範例

參照 `TokenGuard` 的設計，實作一個簡單的 API Token 驗證 Guard。從請求 Header 或 Query 參數取得 Token，並經由 `UserProvider` 解析出使用者。

<Steps>
  <Step title="建立 Guard 類別">
    於 `app/Auth` 目錄建立 Guard 類別。

    ```php theme={null}
    <?php

    namespace App\Auth;

    use Illuminate\Auth\GuardHelpers;
    use Illuminate\Contracts\Auth\Guard;
    use Illuminate\Contracts\Auth\UserProvider;
    use Illuminate\Http\Request;

    class ApiTokenGuard implements Guard
    {
        use GuardHelpers;

        protected Request $request;

        public function __construct(UserProvider $provider, Request $request)
        {
            $this->provider = $provider;
            $this->request = $request;
        }

        /**
         * 回傳當前已驗證的使用者
         */
        public function user(): ?\Illuminate\Contracts\Auth\Authenticatable
        {
            // 若已有快取的使用者則直接回傳
            if (! is_null($this->user)) {
                return $this->user;
            }

            $token = $this->getTokenForRequest();

            if (empty($token)) {
                return null;
            }

            // 經由 UserProvider 取得使用者
            $this->user = $this->provider->retrieveByCredentials([
                'api_token' => $token,
            ]);

            return $this->user;
        }

        /**
         * 僅進行憑證驗證（不執行登入）
         */
        public function validate(array $credentials = []): bool
        {
            if (empty($credentials['api_token'])) {
                return false;
            }

            return (bool) $this->provider->retrieveByCredentials($credentials);
        }

        /**
         * 由請求取得 Token
         *
         * 優先順序：Bearer Header → Query 參數 → 請求 Body
         */
        protected function getTokenForRequest(): ?string
        {
            $token = $this->request->bearerToken();

            if (empty($token)) {
                $token = $this->request->query('api_token');
            }

            if (empty($token)) {
                $token = $this->request->input('api_token');
            }

            return $token ?: null;
        }

        /**
         * 替換 Request 實例
         */
        public function setRequest(Request $request): static
        {
            $this->request = $request;
            return $this;
        }
    }
    ```
  </Step>

  <Step title="於服務提供者註冊 Guard">
    在 `AppServiceProvider` 的 `boot()` 方法中使用 `Auth::extend()` 註冊 Guard。

    ```php theme={null}
    <?php

    namespace App\Providers;

    use App\Auth\ApiTokenGuard;
    use Illuminate\Contracts\Foundation\Application;
    use Illuminate\Support\Facades\Auth;
    use Illuminate\Support\ServiceProvider;

    class AppServiceProvider extends ServiceProvider
    {
        public function boot(): void
        {
            Auth::extend('api-token', function (Application $app, string $name, array $config) {
                // 以 Auth::createUserProvider() 解析 config 中的 provider
                $provider = Auth::createUserProvider($config['provider'] ?? 'users');

                return new ApiTokenGuard($provider, $app->make('request'));
            });
        }
    }
    ```

    <Info>
      `Auth::createUserProvider()` 會讀取 `config/auth.php` 的 `providers` 設定，回傳對應的 `UserProvider` 實例。只要不建立自訂 Provider，透過此呼叫方式即可使用標準的 `EloquentUserProvider`。
    </Info>
  </Step>

  <Step title="於 config/auth.php 設定 Guard">
    在 `config/auth.php` 加入新的 Guard。

    ```php theme={null}
    'guards' => [
        'web' => [
            'driver'   => 'session',
            'provider' => 'users',
        ],

        // 新增的自訂 Guard
        'api' => [
            'driver'   => 'api-token', // 與 Auth::extend() 的第 1 個引數一致
            'provider' => 'users',
        ],
    ],
    ```
  </Step>

  <Step title="於路由套用 Guard">
    在 `auth` 中介層指定 Guard 名稱。

    ```php theme={null}
    // routes/api.php
    use Illuminate\Support\Facades\Route;

    Route::middleware('auth:api')->group(function () {
        Route::get('/user', function () {
            return auth()->user();
        });

        Route::get('/posts', [\App\Http\Controllers\PostController::class, 'index']);
    });
    ```

    要在 Controller 或程式碼中使用特定 Guard，可呼叫 `Auth::guard('api')` 或 `auth('api')`。

    ```php theme={null}
    $user = Auth::guard('api')->user();
    ```
  </Step>
</Steps>

## 以 Closure 定義簡易 Guard

使用 `Auth::viaRequest()` 可以不建立類別，僅以 closure 定義簡單的 Guard。適合原型設計或極為簡易的驗證。

```php theme={null}
use Illuminate\Http\Request;
use App\Models\User;

// 位於 AppServiceProvider::boot() 內
Auth::viaRequest('custom-token', function (Request $request): ?User {
    $token = $request->bearerToken();

    if (empty($token)) {
        return null;
    }

    return User::where('api_token', $token)->first();
});
```

`config/auth.php` 的設定：

```php theme={null}
'guards' => [
    'api' => [
        'driver' => 'custom-token',
    ],
],
```

<Warning>
  以 `Auth::viaRequest()` 定義的 Guard 不使用 `UserProvider`，因此 `retrieveById()` 等 Provider 方法無法運作。正式環境建議使用透過 `Auth::extend()` 的類別式 Guard。
</Warning>

## 自訂 `UserProvider` 的實作

若要從資料庫以外的來源（外部 API、LDAP 等）取得使用者資訊，可實作 `Illuminate\Contracts\Auth\UserProvider` 介面。

```php theme={null}
<?php

namespace App\Auth;

use App\Models\User;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;

class ApiUserProvider implements UserProvider
{
    public function __construct(
        protected string $apiBaseUrl,
        protected string $model = User::class,
    ) {}

    /**
     * 以 ID 取得使用者
     */
    public function retrieveById(mixed $identifier): ?Authenticatable
    {
        return ($this->model)::find($identifier);
    }

    /**
     * 以「記住登入」Token 取得使用者
     * 不使用 session 的 Guard 只需回傳 null 即可
     */
    public function retrieveByToken(mixed $identifier, string $token): ?Authenticatable
    {
        return null;
    }

    /**
     * 更新「記住登入」Token
     * 不使用 session 的 Guard 什麼都不做即可
     */
    public function updateRememberToken(Authenticatable $user, string $token): void {}

    /**
     * 以憑證取得使用者
     * 由 Guard 的 validate() 與 user() 呼叫
     */
    public function retrieveByCredentials(array $credentials): ?Authenticatable
    {
        if (empty($credentials['api_token'])) {
            return null;
        }

        // 於外部 API 驗證 Token 並取得使用者資訊的範例
        $response = \Illuminate\Support\Facades\Http::withToken($credentials['api_token'])
            ->get("{$this->apiBaseUrl}/auth/me");

        if (! $response->successful()) {
            return null;
        }

        $data = $response->json();

        // 對應本地 DB 使用者，或動態產生模型
        return User::firstOrCreate(
            ['external_id' => $data['id']],
            ['name' => $data['name'], 'email' => $data['email']],
        );
    }

    /**
     * 驗證取得的使用者憑證
     * 對 Token 驗證，只要 retrieveByCredentials 成功即可回傳 true
     */
    public function validateCredentials(Authenticatable $user, array $credentials): bool
    {
        return true;
    }

    /**
     * 確認是否需要重新雜湊密碼
     * Token 驗證中恆為 false 即可
     */
    public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false): void {}
}
```

### 自訂 `UserProvider` 的註冊

```php theme={null}
// AppServiceProvider::boot()
Auth::provider('api-user', function (Application $app, array $config) {
    return new \App\Auth\ApiUserProvider(
        config('services.auth_api.base_url'),
        $config['model'] ?? \App\Models\User::class,
    );
});
```

在 `config/auth.php` 的 `providers` 區段加入：

```php theme={null}
'providers' => [
    'users' => [
        'driver' => 'eloquent',
        'model'  => App\Models\User::class,
    ],

    // 自訂 Provider
    'api-users' => [
        'driver' => 'api-user',
        'model'  => App\Models\User::class,
    ],
],
```

將 Guard 與 Provider 搭配：

```php theme={null}
'guards' => [
    'api' => [
        'driver'   => 'api-token',
        'provider' => 'api-users', // 指定自訂 Provider
    ],
],
```

## 實務使用情境

### 多重驗證（管理員與一般使用者使用不同 Guard）

<Steps>
  <Step title="建立管理員 Model">
    準備管理員用的 Eloquent Model。繼承 `Authenticatable` 即可與 `Auth` 系統整合。

    ```bash theme={null}
    php artisan make:model Admin -m
    ```

    ```php theme={null}
    <?php

    namespace App\Models;

    use Illuminate\Foundation\Auth\User as Authenticatable;

    class Admin extends Authenticatable
    {
        protected $fillable = ['name', 'email', 'password'];

        protected $hidden = ['password', 'remember_token'];
    }
    ```
  </Step>

  <Step title="設定 config/auth.php">
    ```php theme={null}
    'guards' => [
        'web' => [
            'driver'   => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver'   => 'session',
            'provider' => 'admins', // 管理員用 Provider
        ],
    ],

    'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model'  => App\Models\User::class,
        ],
        'admins' => [
            'driver' => 'eloquent',
            'model'  => App\Models\Admin::class, // 管理員 Model
        ],
    ],
    ```
  </Step>

  <Step title="設定路由與中介層">
    ```php theme={null}
    // routes/web.php

    // 一般使用者路由（預設 web Guard）
    Route::middleware('auth')->group(function () {
        Route::get('/dashboard', [DashboardController::class, 'index']);
    });

    // 管理員路由（admin Guard）
    Route::prefix('admin')->middleware('auth:admin')->group(function () {
        Route::get('/dashboard', [AdminDashboardController::class, 'index']);
    });
    ```
  </Step>

  <Step title="指定 Guard 撰寫登入處理">
    ```php theme={null}
    <?php

    namespace App\Http\Controllers\Admin;

    use Illuminate\Http\Request;
    use Illuminate\Support\Facades\Auth;

    class LoginController extends Controller
    {
        public function store(Request $request)
        {
            $credentials = $request->validate([
                'email'    => 'required|email',
                'password' => 'required',
            ]);

            // 明確指定 admin Guard 進行 attempt
            if (Auth::guard('admin')->attempt($credentials)) {
                $request->session()->regenerate();
                return redirect()->intended('/admin/dashboard');
            }

            return back()->withErrors(['email' => '登入資訊有誤。']);
        }

        public function destroy(Request $request)
        {
            Auth::guard('admin')->logout();
            $request->session()->invalidate();
            $request->session()->regenerateToken();

            return redirect('/admin/login');
        }
    }
    ```
  </Step>
</Steps>

### 以 JWT Token 進行外部 API 驗證

使用外部 JWT 驗證服務的自訂 Guard 實作範例。

```php theme={null}
<?php

namespace App\Auth;

use App\Models\User;
use Illuminate\Auth\GuardHelpers;
use Illuminate\Contracts\Auth\Guard;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Http;

class JwtGuard implements Guard
{
    use GuardHelpers;

    protected Request $request;
    protected ?array $payload = null;

    public function __construct(UserProvider $provider, Request $request)
    {
        $this->provider = $provider;
        $this->request  = $request;
    }

    public function user(): ?\Illuminate\Contracts\Auth\Authenticatable
    {
        if (! is_null($this->user)) {
            return $this->user;
        }

        $token = $this->request->bearerToken();

        if (empty($token)) {
            return null;
        }

        // 於外部服務驗證 JWT
        $payload = $this->verifyToken($token);

        if (is_null($payload)) {
            return null;
        }

        $this->payload = $payload;

        $this->user = $this->provider->retrieveById($payload['sub']);

        return $this->user;
    }

    public function validate(array $credentials = []): bool
    {
        if (empty($credentials['token'])) {
            return false;
        }

        return ! is_null($this->verifyToken($credentials['token']));
    }

    /**
     * 驗證 JWT 並取得 payload
     */
    protected function verifyToken(string $token): ?array
    {
        $response = Http::withToken($token)
            ->get(config('services.auth.verify_url'));

        if (! $response->successful()) {
            return null;
        }

        return $response->json();
    }

    /**
     * 回傳已驗證的 JWT payload
     */
    public function payload(): ?array
    {
        return $this->payload;
    }
}
```

於 `AppServiceProvider` 註冊：

```php theme={null}
Auth::extend('jwt', function (Application $app, string $name, array $config) {
    return new \App\Auth\JwtGuard(
        Auth::createUserProvider($config['provider'] ?? 'users'),
        $app->make('request'),
    );
});
```

<Tip>
  也可以像 `Auth::guard('jwt')->payload()` 這樣存取自訂 Guard 專屬的方法。因為 `Auth::guard()` 回傳的是 Guard 實例本身，所以可呼叫非介面所定義的方法。
</Tip>

## 測試

自訂 Guard 的單元測試中，透過 mock `UserProvider` 來確認 Guard 的行為。

```php theme={null}
<?php

namespace Tests\Unit\Auth;

use App\Auth\ApiTokenGuard;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\UserProvider;
use Illuminate\Http\Request;
use PHPUnit\Framework\MockObject\MockObject;
use Tests\TestCase;

class ApiTokenGuardTest extends TestCase
{
    private UserProvider&MockObject $provider;
    private ApiTokenGuard $guard;

    protected function setUp(): void
    {
        parent::setUp();

        $this->provider = $this->createMock(UserProvider::class);
    }

    public function test_user_returns_null_when_no_token(): void
    {
        $request = Request::create('/');
        $guard   = new ApiTokenGuard($this->provider, $request);

        $this->assertNull($guard->user());
    }

    public function test_user_returns_user_with_valid_bearer_token(): void
    {
        $mockUser = $this->createMock(Authenticatable::class);

        $this->provider
            ->expects($this->once())
            ->method('retrieveByCredentials')
            ->with(['api_token' => 'valid-token'])
            ->willReturn($mockUser);

        $request = Request::create('/');
        $request->headers->set('Authorization', 'Bearer valid-token');

        $guard = new ApiTokenGuard($this->provider, $request);

        $this->assertSame($mockUser, $guard->user());
    }

    public function test_check_returns_false_when_no_token(): void
    {
        $request = Request::create('/');
        $guard   = new ApiTokenGuard($this->provider, $request);

        $this->assertFalse($guard->check());
    }

    public function test_validate_returns_false_without_api_token_credential(): void
    {
        $request = Request::create('/');
        $guard   = new ApiTokenGuard($this->provider, $request);

        $this->assertFalse($guard->validate([]));
    }
}
```

透過 `ActingAs` 的功能測試中，可以將使用者設定到特定的 Guard。

```php theme={null}
// 於特定 Guard 中驗證使用者以進行測試
$this->actingAs($user, 'api')
    ->getJson('/api/user')
    ->assertOk();
```

## 相關頁面

<Card title="驗證（入門）" icon="shield-check" href="/zh-TW/authentication">
  查看啟動套件及標準的驗證流程。
</Card>

<Card title="服務容器" icon="box" href="/zh-TW/service-container">
  理解在 Guard 註冊中使用的服務容器機制。
</Card>


## Related topics

- [自訂驗證規則](/zh-TW/advanced/custom-validation-rules.md)
- [GeneratorCommand — 自訂 make: 指令的實作](/zh-TW/advanced/generator-command.md)
- [Laravel Fortify 與啟動套件](/zh-TW/advanced/fortify.md)
- [驗證](/zh-TW/validation.md)
- [進階主題](/zh-TW/advanced/index.md)
