iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 24
2
Software Development

Laravel 原始碼分析系列 第 24

分析 Auth(1)

  • 分享至 

  • xImage
  •  

Auth 是筆者認為,在 Laravel 開放的 Illuminate 套件包裡(Support 除外),前三名複雜的。

註一:Support 除外的原因是,它比較像是 helper,類別大部分都能獨立運作,如 Facade。
註二:筆者認為的前三名:DatabaseAuthQueue

之前幾天在分析元件時,都是很容易知道如何開始,因為都有進入點(entry point)。無論是從初始化實例開始或是實際 Laravel 如何使用這個元件,都是有辦法找出開始分析的路徑。但 Auth,但類別本身較複雜,加上筆者並沒有使用過 Auth 套件,所以一開始也不知道該如何下手。

這時還有一種方法:看文件。文件通常會用容易理解的方法,讓開發者可以快速了解元件會有哪些角色,以及它們如何互動。從文件去了解套件如何開始分析,是有幫助的。

Authentication

文件裡可以大致知道 Auth 提供兩種類型:AuthenticationAuthorization,今天就先從 Authentication 開始。

裡面有提到的關鍵角色是 Guard 與 Providers。Guard 處理驗證,而 Providers 是提供資料讓 Guard 驗證。相關的 UML 圖如下:

@startuml
interface Illuminate\Contracts\Auth\Authenticatable
interface Illuminate\Contracts\Auth\Factory
interface Illuminate\Contracts\Auth\Guard
interface Illuminate\Contracts\Auth\StatefulGuard
interface Illuminate\Contracts\Auth\UserProvider

class DatabaseUserProvider
class EloquentUserProvider
class GenericUser
class RequestGuard
class SessionGuard
class TokenGuard
class AuthManager

Illuminate\Contracts\Auth\Factory <|.. AuthManager
Illuminate\Contracts\Auth\Factory -> Illuminate\Contracts\Auth\Guard
Illuminate\Contracts\Auth\Factory --> Illuminate\Contracts\Auth\StatefulGuard
Illuminate\Contracts\Auth\Guard <|-- Illuminate\Contracts\Auth\StatefulGuard
Illuminate\Contracts\Auth\Guard o- Illuminate\Contracts\Auth\Authenticatable
Illuminate\Contracts\Auth\Guard <|.. RequestGuard
Illuminate\Contracts\Auth\Guard <|.. TokenGuard
Illuminate\Contracts\Auth\StatefulGuard <|.. SessionGuard
Illuminate\Contracts\Auth\Authenticatable <- Illuminate\Contracts\Auth\UserProvider
Illuminate\Contracts\Auth\Authenticatable <|.. GenericUser
Illuminate\Contracts\Auth\UserProvider <|.. DatabaseUserProvider
Illuminate\Contracts\Auth\UserProvider <|.. EloquentUserProvider
@enduml

從圖可以知道,UserProvider 是建構 Authenticatable 的角色;Factory 是負責建構 Guard;而 Authenticatable 則是提供給 Guard 做驗證用。

Service provider

Auth 套件主要的 service provider 是 AuthServiceProvider

$this->app->singleton('auth', function ($app) {
    $app['auth.loaded'] = true;

    // 建構 UML 圖裡的 AuthManager 類別
    return new AuthManager($app);
});

$this->app->singleton('auth.driver', function ($app) {
    // 取得預設的 guard 作為 driver
    return $app['auth']->guard();
});

$this->app->bind(AuthenticatableContract::class, function ($app) {
    // 當需要取得 Authenticatable 物件時,會使用 AuthManager 的 userResolver 解析
    return call_user_func($app['auth']->userResolver());
});

$this->app->rebinding('request', function ($app, $request) {
    // 將全域的 $request 多設定 userResolver 的參數
    $request->setUserResolver(function ($guard = null) use ($app) {
        return call_user_func($app['auth']->userResolver(), $guard);
    });
});

另外,因為已經知道 Facade 的用法了,裡面有一個 Auth Facade 是對應到 Container 綁定的 auth,也就是 AuthManager

Authenticate Middleware

知道類別如何綁定後,再來了解它如何被使用。Authenticate Middleware 會檢查使用者是否有驗證,從 handle()authenticate() 可以大概知道需要呼叫哪些參數

// 額外參數使用 ... ,代表 guards 是可以設定很多筆的
public function handle($request, Closure $next, ...$guards)
{
    $this->authenticate($request, $guards);

    return $next($request);
}

protected function authenticate($request, array $guards)
{
    // 如果 middleware 沒有設定的話,就使用 null ,也就是預設值
    if (empty($guards)) {
        $guards = [null];
    }

    // 依續檢查每一個 guard
    foreach ($guards as $guard) {
        // 當有檢查過,就呼叫 shouldUse()
        if ($this->auth->guard($guard)->check()) {
            return $this->auth->shouldUse($guard);
        }
    }

    // 驗證失敗的例外
    throw new AuthenticationException(
        'Unauthenticated.', $guards, $this->redirectTo($request)
    );
}

來看 guard() 是如何取得 driver 的,其實跟 SessionManger 或 LogManager 都非常像:

public function guard($name = null)
{
    // 是 null 就取預設的 driver
    $name = $name ?: $this->getDefaultDriver();

    // 如果不存在的話就解析一下
    return $this->guards[$name] ?? $this->guards[$name] = $this->resolve($name);
}

public function getDefaultDriver()
{
    // Laravel 預設的 driver 叫 `web` 
    return $this->app['config']['auth.defaults.guard'];
}

resolve() 原始碼:

protected function resolve($name)
{
    // 取得設定,如 web 的預設設定是:['driver' => 'session', 'provider' => 'users']
    $config = $this->getConfig($name);

    if (is_null($config)) {
        throw new InvalidArgumentException("Auth guard [{$name}] is not defined.");
    }

    // 有自定義的建置方法就使用
    if (isset($this->customCreators[$config['driver']])) {
        return $this->callCustomCreator($name, $config);
    }

    // 使用預設的建置方法
    $driverMethod = 'create'.ucfirst($config['driver']).'Driver';

    if (method_exists($this, $driverMethod)) {
        return $this->{$driverMethod}($name, $config);
    }

    throw new InvalidArgumentException("Auth driver [{$config['driver']}] for guard [{$name}] is not defined.");
}

web 的 driver 是 session,對應的方法是 createSessionDriver()

public function createSessionDriver($name, $config)
{
    // 建構 UserProvider
    $provider = $this->createUserProvider($config['provider'] ?? null);

    // 建構 SessionGuard,也是 UML 有提到的實作
    $guard = new SessionGuard($name, $provider, $this->app['session.store']);

    // 如果 guard 有定義 setCookieJar 方法,就呼叫一下
    if (method_exists($guard, 'setCookieJar')) {
        $guard->setCookieJar($this->app['cookie']);
    }

    // 如果 guard 有定義 setDispatcher 方法,就呼叫一下
    if (method_exists($guard, 'setDispatcher')) {
        $guard->setDispatcher($this->app['events']);
    }

    // 如果 guard 有定義 setRequest 方法,就呼叫一下
    if (method_exists($guard, 'setRequest')) {
        // 這裡會觸發 rebind 事件,並把 request 設定給 $guard
        $guard->setRequest($this->app->refresh('request', $guard, 'setRequest'));
    }

    return $guard;
}

這裡的寫法其實是有點奇怪的。$guard 很明確使用 new SessionGuard() 建構,該實例會有什麼方法,是可以預期的,因此下面 set 的相關方法,使用 method_exists() 判斷就顯得多餘。

經過上面的過程,guard() 就能取得 SessionGuard 實例了。

今天休息一下,明天再繼續看這個 guard 是如何檢查的。


上一篇
分析 AliasLoader
下一篇
分析 Auth(2)
系列文
Laravel 原始碼分析46
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言