Laravel 驗證系統的內部結構
Auth Facade 與 AuthManager
Auth Facade 是 Illuminate\Auth\AuthManager 的代理。AuthManager 以 Driver 模式管理多個 Guard,並依照 config/auth.php 的設定產生並快取適當的 Guard 實例。
// 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。
Guard 介面(Illuminate\Contracts\Auth\Guard)
Guard 介面(Illuminate\Contracts\Auth\Guard)
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);
}
StatefulGuard 介面(Illuminate\Contracts\Auth\StatefulGuard)
StatefulGuard 介面(Illuminate\Contracts\Auth\StatefulGuard)
StatefulGuard 繼承 Guard,並加入使用 session 或 cookie 維持登入狀態所需的方法。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();
}
對於 API 驗證或自訂 Token 驗證等不需 session 的 Guard,僅需實作
Guard 即可。若如管理員登入等需要 session 時,則實作 StatefulGuard。自訂 Guard 的實作
GuardHelpers trait
由於 Guard 介面的 check()、guest()、id()、hasUser() 幾乎皆為相同實作,Laravel 提供了 Illuminate\Auth\GuardHelpers trait。使用此 trait 後,必要實作的內容便可縮減至 user() 與 validate() 兩個方法。
// 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 解析出使用者。
1
建立 Guard 類別
於
app/Auth 目錄建立 Guard 類別。<?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;
}
}
2
於服務提供者註冊 Guard
在
AppServiceProvider 的 boot() 方法中使用 Auth::extend() 註冊 Guard。<?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'));
});
}
}
Auth::createUserProvider() 會讀取 config/auth.php 的 providers 設定,回傳對應的 UserProvider 實例。只要不建立自訂 Provider,透過此呼叫方式即可使用標準的 EloquentUserProvider。3
於 config/auth.php 設定 Guard
在
config/auth.php 加入新的 Guard。'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
// 新增的自訂 Guard
'api' => [
'driver' => 'api-token', // 與 Auth::extend() 的第 1 個引數一致
'provider' => 'users',
],
],
4
於路由套用 Guard
在 要在 Controller 或程式碼中使用特定 Guard,可呼叫
auth 中介層指定 Guard 名稱。// 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']);
});
Auth::guard('api') 或 auth('api')。$user = Auth::guard('api')->user();
以 Closure 定義簡易 Guard
使用Auth::viaRequest() 可以不建立類別,僅以 closure 定義簡單的 Guard。適合原型設計或極為簡易的驗證。
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 的設定:
'guards' => [
'api' => [
'driver' => 'custom-token',
],
],
以
Auth::viaRequest() 定義的 Guard 不使用 UserProvider,因此 retrieveById() 等 Provider 方法無法運作。正式環境建議使用透過 Auth::extend() 的類別式 Guard。自訂 UserProvider 的實作
若要從資料庫以外的來源(外部 API、LDAP 等)取得使用者資訊,可實作 Illuminate\Contracts\Auth\UserProvider 介面。
<?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 的註冊
// 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 區段加入:
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// 自訂 Provider
'api-users' => [
'driver' => 'api-user',
'model' => App\Models\User::class,
],
],
'guards' => [
'api' => [
'driver' => 'api-token',
'provider' => 'api-users', // 指定自訂 Provider
],
],
實務使用情境
多重驗證(管理員與一般使用者使用不同 Guard)
1
建立管理員 Model
準備管理員用的 Eloquent Model。繼承
Authenticatable 即可與 Auth 系統整合。php artisan make:model Admin -m
<?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'];
}
2
設定 config/auth.php
'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
],
],
3
設定路由與中介層
// 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']);
});
4
指定 Guard 撰寫登入處理
<?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');
}
}
以 JWT Token 進行外部 API 驗證
使用外部 JWT 驗證服務的自訂 Guard 實作範例。<?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 註冊:
Auth::extend('jwt', function (Application $app, string $name, array $config) {
return new \App\Auth\JwtGuard(
Auth::createUserProvider($config['provider'] ?? 'users'),
$app->make('request'),
);
});
也可以像
Auth::guard('jwt')->payload() 這樣存取自訂 Guard 專屬的方法。因為 Auth::guard() 回傳的是 Guard 實例本身,所以可呼叫非介面所定義的方法。測試
自訂 Guard 的單元測試中,透過 mockUserProvider 來確認 Guard 的行為。
<?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。
// 於特定 Guard 中驗證使用者以進行測試
$this->actingAs($user, 'api')
->getJson('/api/user')
->assertOk();
相關頁面
驗證(入門)
查看啟動套件及標準的驗證流程。
服務容器
理解在 Guard 註冊中使用的服務容器機制。