分析 Auth(4)--Authorization

前面我們看完了驗證(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 類別是怎麼串接上去的。