iT邦幫忙

2021 iThome 鐵人賽

DAY 25
0

昨天在設定完身分驗證的架構後已經可以在路由上加 auth 中介層來驗證使用者的身分是否允許訪問。

Route::get('/flights', function () {
    // 通過 admin 檢查的用戶才能進入路由
})->middleware('auth:admin');

不過除了只看身分外,Laravel 可以設定更詳細的權限檢查,像是檢查使用者是否擁有這筆 Todo ,是的話才能進行刪除等等。

定義權限的方式分為 Guard 跟 Policy ,兩者相較下 Policy 的架構會比較好管理跟維護,所以這裡介紹的重點會放在 Policy 上,Guard 就只稍微介紹點,不過其實 Policy 也是 Guard 的應用就是。

Guard

Guard 就像路由,一筆筆的定義某個行徑對應的檢查函式,一般都定義在 AuthServiceProvider 中。

/app/Providers/AuthServiceProvider.php

use App\Models\Todo;
use App\Models\User;
use Illuminate\Support\Facades\Gate;

public function boot()
{
    $this->registerPolicies();

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

進行 update-todo 檢查時,會檢查傳入的 user 跟 todo ,若 todo 是屬於 user 的則回傳 true 表示權限許可,否則為 false。

use Illuminate\Support\Facades\Gate;

public function update(Request $request, Todo $todo)
{

    if (! Gate::allows('update-todo', $todo)) {
        abort(403);
    }

    // continue update process ...
}

接著就能用 Gate::allows 進行檢查,這邊要注意我們並沒有傳入 $user 參數,因為 Gate Facade 會自動從 Auth 取得目前登入的用戶代入。

如果想驗證的不是目前登入的使用者,加上 forUser 方法指定要驗證的使用者

if (! Gate::forUser($user)->allows('update-todo', $todo)) {
  abort(403);
}

Policy

Policy 用於制定使用者對於某個 Model 的操作權限,其本質就是一組針對特定 Model 的 Gate 。

如果用指令創建 Model 的時候選擇了全餐 -a ,當中就會包含對應的 Policy ,如果沒有的話,就另外建立。

sail artisan make:policy TodoPolicy --model Todo

如果在建立 Policy 時有指定 Model 的話,就會預先產生一些方法像是 view,create,update 等。

註冊 Policy

Policy 產生後要在 AuthServiceProvider 中登記對特定 Model 進行權限檢查時要用哪個 Policy 。

/app/Providers/.php

<?php

namespace App\Providers;

use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\Facades\Auth; 

class AuthServiceProvider extends ServiceProvider
{
    protected $policies = [
        App\Models\Todo::class => App\Policies\TodoPolicy::class,
    ];

    //...
}

除了這裡之外也能動態的用 Gate::policy() 定義,例如:

$user = Auth::user();

if($user instanceof User){
    \Gate::policy( Todo::class, TodoUserPolicy::class); 
}else if($user instanceof Admin){         
    \Gate::policy( Todo::class, TodoAdminPolicy::class); 
}

如果有複數身分 Model 可以考慮這麼做,不過一般都建議不要用多個身分驗證模型提高複雜度。

自動偵測

如果完全沒有註冊 Model 對應的 Policy, Guard 會試圖自動找出對應的 Policy ,尋找步驟如下:

  1. 從 Model 所在目錄向上查詢名為 Policies 的目錄。
  2. 在 Policies 目錄中找名稱以 Policy 結尾的檔案,找是否有 Policy 前的字串與 Model 名稱相同的。

可以看到用指令產生的 Policy 都符合上述的步驟,所以一般其實不用特地到 AuthServiceProvider 登記 Policy。

撰寫 Policy

就跟寫 Guard 的 callback 函式相同,第一個參數傳入 User ,之後看要傳入哪個 Model。

public function update(User $user, Todo $todo)
{
      return $user->id === $todo->user_id;
}

$user 要視驗證的身分標記對應的 Model ,一般來說是 User ,不過如果 Policy 用來對應 Admin 模型的驗證的話就要改成 Admin。

public function update(Admin $admin, Todo $todo)
{
      return $admin->id === $todo->user_id;
}

Responses

權限檢查除了回傳 true / false 以外,也可以回傳帶有更多資訊的 Response 物件。注意是用 Auth 的 Response 。

use Illuminate\Auth\Access\Response;

public function update(User $user, Todo $todo)
{
      return $user->id === $todo->user_id
            ? Response::allow()
            : Response::deny('You do not own this post.');
}

回傳 Response 後如果用 can , allows 等方法,回傳還是一般的 true / false,要看到 Response 的內容的話要用 Gate 的 inspect 方法。

$response = Gate::inspect('update', $todo);

if ($response->allowed()) {
    // The action is authorized...
} else {
    echo $response->message();
}

Policy Filter

Policy 中有特別的函式 before ,顧名思義會在進行任何檢查前執行,如果 before 回傳 null 以外的值的話該值會被當成這次檢查的回傳值。

public function before(User $user, $ability)
{
    // 如果用戶是 admin 就跳過檢查一律回傳 true
    if ($user->isAdministrator()) {
        return true;
    }
}

用 Policy 驗證

寫好之後終於可以用啦,用法跟 Guard 相似不過是從 User 出發進行檢查,並且不是用 allows 而是 can 跟 cannot 等方法檢查。

allows/ denies/ inspect 是 Guard 類別的方法
can/ cannot 是 Authorizable 類別(User 繼承的類別)的方法
注意別用錯了

if ($request->user()->cannot('update', $todo)) {
    abort(403);
}

另外當驗證的方法不用除了 User 以外的資料時,需要帶入 Model 名稱才知道要找哪個 Policy 進行驗證。

if ($request->user()->cannot('create', Todo::class)) {
    abort(403);
}

上一篇
身分驗證
下一篇
信件
系列文
Laravel 實務筆記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言