iT邦幫忙

2019 iT 邦幫忙鐵人賽

DAY 27
1
Software Development

Laravel 原始碼分析系列 第 27

分析 Auth(4)--Authorization

  • 分享至 

  • xImage
  •  

前面我們看完了驗證(Authenticate)的實作,今天來看授權(Authorization)。

官方文件可以大概知道它的主要角色有兩個:Gate 和 Policy。Policy 並沒有特定的介面定義,甚至也可以用 Closure 取代,而 Gate 有。從 Gate 延伸出來的 UML 圖如下:

@startuml
interface Illuminate\Contracts\Auth\Access\Authorizable
interface Illuminate\Contracts\Auth\Access\Gate
interface Illuminate\Contracts\Auth\Authenticatable

class Access\Gate
class Access\Response
class Illuminate\Foundation\Auth\Access\Authorizable

Illuminate\Contracts\Auth\Access\Gate <|.. Access\Gate
Illuminate\Contracts\Auth\Access\Gate -> Illuminate\Contracts\Auth\Authenticatable
Illuminate\Foundation\Auth\Access\Authorizable -> Illuminate\Contracts\Auth\Access\Gate
Illuminate\Contracts\Auth\Access\Authorizable <|.. Illuminate\Foundation\Auth\Access\Authorizable
Access\Gate -> Access\Response
@enduml

建構的過程也寫在 AuthServiceProvider

$this->app->singleton(GateContract::class, function ($app) {
    // 建構子都是設定參數
    return new Gate($app, function () use ($app) {
        // 把 AuthManager 的 userResolver 拿來用
        return call_user_func($app['auth']->userResolver());
    });
});

Facade 是 Gate:

class Gate extends Facade
{
    protected static function getFacadeAccessor()
    {
        return GateContract::class;
    }
}

這次的類別關係,並沒想像中的錯綜複雜,但似乎也是一個需要花時間理解的元件。

了解 Gate

參考官方文件的範例:

// Boot 階段執行

// Closure 定義法
Gate::define('update-post', function ($user, $post) {
    return $user->id == $post->user_id;
});

// Policy 定義法
Gate::define('update-post', 'PostPolicy@update');

// 確認目前驗證過的 user 可以執行某件事
if (Gate::allows('update-post', $post)) {
}

// 確認目前驗證過的 user 不能執行某件事
if (Gate::denies('update-post', $post)) {
}

// 針對特定的 user
Gate::forUser($user)->allows('update-post', $post));
Gate::forUser($user)->denies('update-post', $post));

後面會先以 Closure 定義法分析。開始來看這一系列的程式碼是如何運作的。首先看 define()

// User 能不能使用某個功能,在 Gate 裡面稱之為 `ability`
public function define($ability, $callback)
{
    if (is_callable($callback)) {
        // 如果是 callable,就放在一個 lookup 表裡
        $this->abilities[$ability] = $callback;
    } elseif (is_string($callback)) {
        // 如果是 string 就建立另一個 callback,runtime 再來解析 string
        $this->abilities[$ability] = $this->buildAbilityCallback($ability, $callback);
    } else {
        throw new InvalidArgumentException("Callback must be a callable or a 'Class@method' string.");
    }

    return $this;
}

判斷有沒有授權,會使用 allows()denies()

public function allows($ability, $arguments = [])
{
    return $this->check($ability, $arguments);
}

public function denies($ability, $arguments = [])
{
    return ! $this->allows($ability, $arguments);
}

很好懂,就不說明了。再來看 check()

public function check($abilities, $arguments = [])
{
    // 把 ability 轉換成 Collection 後,再看是否所有 ability 跑 callback 都是 true
    return collect($abilities)->every(function ($ability) use ($arguments) {
        try {
            // 呼叫 raw() 取得結果
            return (bool) $this->raw($ability, $arguments);
        } catch (AuthorizationException $e) {
            return false;
        }
    });
}

raw() 的原始碼:

public function raw($ability, $arguments = [])
{
    // 把 $arguments 重新包成 array
    $arguments = Arr::wrap($arguments);

    // 解析 user 實例
    $user = $this->resolveUser();

    // 呼叫所有 before callback,直到 callback 回傳不是 null 或是沒有為止 
    $result = $this->callBeforeCallbacks(
        $user, $ability, $arguments
    );

    // 如果依然沒結果的話,就找出對應的 callback 並執行
    if (is_null($result)) {
        $result = $this->callAuthCallback($user, $ability, $arguments);
    }

    // 呼叫 after callback,最後再把 result 回傳
    return $this->callAfterCallbacks(
        $user, $ability, $arguments, $result
    );
}

依續看三種 callback 做了什麼事:

protected function callBeforeCallbacks($user, $ability, array $arguments)
{
    // 將所有傳入的參數合併成一個 array
    $arguments = array_merge([$user, $ability], [$arguments]);

    // 依序呼叫 callback
    foreach ($this->beforeCallbacks as $before) {
        // 如果這個 user 不能使用的話就跳過
        if (! $this->canBeCalledWithUser($user, $before)) {
            continue;
        }

        // 可以的話就取得結果,如果有結果的話就結束迴圈並回傳 
        if (! is_null($result = $before(...$arguments))) {
            return $result;
        }
    }
}

protected function callAuthCallback($user, $ability, array $arguments)
{
    // 解析 callback
    $callback = $this->resolveAuthCallback($user, $ability, $arguments);

    // 呼叫 callback
    return $callback($user, ...$arguments);
}

protected function resolveAuthCallback($user, $ability, array $arguments)
{
    // 使用 model 可以取得 policy,且解析 policy callback 也有東西的話,就使用它 
    if (isset($arguments[0]) &&
        ! is_null($policy = $this->getPolicyFor($arguments[0])) &&
        $callback = $this->resolvePolicyCallback($user, $ability, $arguments, $policy)) {
        return $callback;
    }

    // 如果有設定 ability 且可以用這個 callback 的話,就使用它
    if (isset($this->abilities[$ability]) &&
        $this->canBeCalledWithUser($user, $this->abilities[$ability])) {
        return $this->abilities[$ability];
    }

    // 什麼都沒的 callback
    return function () {
        return null;
    };
}
    
protected function callAfterCallbacks($user, $ability, array $arguments, $result)
{
    // 依序呼叫 callback
    foreach ($this->afterCallbacks as $after) {
        // 如果這個 user 不能使用的話就跳過
        if (! $this->canBeCalledWithUser($user, $after)) {
            continue;
        }

        // 呼叫 callback 並取得結果
        $afterResult = $after($user, $ability, $result, $arguments);

        // 如果原本結果是 null 的話,就使用呼叫 callback 過後的結果
        $result = $result ?? $afterResult;
    }

    return $result;
}

感覺關鍵都在 canBeCalledWithUser() 是否回傳 true

protected function canBeCalledWithUser($user, $class, $method = null)
{
    // user 有驗證,才有辦法呼叫
    if (! is_null($user)) {
        return true;
    }

    // 使用者沒登入的話,就得看 policy 或 callback 是否可以未驗證的情況下呼叫
    if (! is_null($method)) {
        return $this->methodAllowsGuests($class, $method);
    }

    return $this->callbackAllowsGuests($class);
}

protected function methodAllowsGuests($class, $method)
{
    try {
        $reflection = new ReflectionClass($class);

        $method = $reflection->getMethod($method);
    } catch (Exception $e) {
        return false;
    }

    if ($method) {
        $parameters = $method->getParameters();

        // 取得 ReflectionMethod 取得參數,再呼叫 parameterAllowsGuests() 確認
        return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
    }

    return false;
}

protected function callbackAllowsGuests($callback)
{
    $parameters = (new ReflectionFunction($callback))->getParameters();
    
    // 取得 callback 參數,再呼叫 parameterAllowsGuests() 確認
    return isset($parameters[0]) && $this->parameterAllowsGuests($parameters[0]);
}

protected function parameterAllowsGuests($parameter)
{
    // 看對應的 method 或 callback 的參數是否可以允許 guest 呼叫
    // 標準也很簡單,只要參數可以允許 null 即可
    return ($parameter->getClass() && $parameter->allowsNull()) ||
           ($parameter->isDefaultValueAvailable() && is_null($parameter->getDefaultValue()));
}

回到一開始的範例:

Gate::define('update-post', function ($user, $post) {
    return $user->id == $post->user_id;
});

if (Gate::allows('update-post', $post)) {
}

這裡的 allows() 將會在上述 resolveAuthCallback() 取得 define() 的 callback,再依 callback 的結果決定 allows() 回傳 bool。

了解了上面的流程後,forUser() 就非常好懂了:

public function forUser($user)
{
    $callback = function () use ($user) {
        return $user;
    };

    return new static(
        $this->container, $callback, $this->abilities,
        $this->policies, $this->beforeCallbacks, $this->afterCallbacks
    );
}

產生一個新的 Gate 實例,只是 user resolver 換掉而已。所有 abilities 等屬性,都跟原本的無異。

以上,Gate 使用 Closure 的流程大概分析完畢,明天再看 Policy 類別是怎麼串接上去的。


上一篇
分析 Auth(3)--客製化驗證機制
下一篇
分析 Auth(5)--Authorization
系列文
Laravel 原始碼分析46
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言