7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Laravelの認可、ゲートとポリシーを使ったルーティングベースのシンプルな実装

7
Posted at

Laravelで開発していて、認証(Authentication)はすぐに理解出来ても、認可(Authorization)はなかなか理解出来ませんでした。
自分の書き方と合ってないのか、どう認可を使えばいいのかわからなかった。
使っていくうちにだんだんと慣れてきましたが、それでも難しい。
そこで今回は、自分の知識の再確認のため、あるAPIを実行出来る権限がログインユーザにあるかどうかを、認可(ゲートとポリシー)を使って実装してみました。

https://laravel.com/docs/6.x/authorization
https://readouble.com/laravel/6.x/ja/authorization.html

使うLaravelのバージョンは6.8.0です。

システムの仕様

  • ユーザは権限を持つ。
  • APIは権限に対して認可されないと実行出来ない。
  • リレーションはuser -- role -< authorizations

データベース

usersテーブルにはuser_role_idカラムがある。

user_rolesテーブル

id name
1 管理者
2 スタッフ

user_role_authorizationsテーブル

id user_role_id route_name
1 1 user_index
2 1 user_create
3 2 user_index

テスト

以下の2つのテストが成功すれば完了とします。
上の表で、スタッフはuser_createへの権限がありません。
したがってtests/Feature/UserCreateTest.phpにおいて、スタッフでuser_createにAPIリクエストして403エラーが返ってくれば成功というわけです:thumbsup:

tests/Feature/UserIndexTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\User;
use App\UserRole;

class UserIndexTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        $this->seed();
    }

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testAdmin()
    {
        $user = factory(User::class)->create(['user_role_id' => UserRole::ADMIN_ROLE_ID]);

        $response = $this->actingAs($user)->getJson('/api/user');

        $response->assertStatus(200);
    }

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testStaff()
    {
        $user = factory(User::class)->create(['user_role_id' => UserRole::STAFF_ROLE_ID]);

        $response = $this->actingAs($user)->getJson('/api/user');

        $response->assertStatus(200);
    }
}
tests/Feature/UserCreateTest.php
<?php

namespace Tests\Feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;
use App\User;
use App\UserRole;

class UserCreateTest extends TestCase
{
    use RefreshDatabase;

    protected function setUp(): void
    {
        parent::setUp();

        $this->seed();
    }

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testAdmin()
    {
        $user = factory(User::class)->create(['user_role_id' => UserRole::ADMIN_ROLE_ID]);

        $response = $this->actingAs($user)->getJson('/api/user/create');

        $response->assertStatus(405);

        $response = $this->actingAs($user)->postJson('/api/user/create');

        $response->assertStatus(200);
    }

    /**
     * A basic feature test example.
     *
     * @return void
     */
    public function testStaff()
    {
        $user = factory(User::class)->create(['user_role_id' => UserRole::STAFF_ROLE_ID]);

        $response = $this->actingAs($user)->postJson('/api/user/create');

        // これが重要
        $response->assertStatus(403);
    }
}

ゲートとポリシー

まずポリシーを作ります。
ここで作るallow()メソッドは引数としてログインしたユーザを受け取ります(メソッド名はなんでも大丈夫)。
返り値はResponse::allow()で許可、Response::deny()で拒否です。

$ php artisan make:policy AllowRequestApi
app/Policies/AllowRequestApi.php
<?php

namespace App\Policies;

use App\User;
use Illuminate\Auth\Access\HandlesAuthorization;
use Illuminate\Auth\Access\Response;
use Route;
use Auth;

class AllowRequestApi
{
    use HandlesAuthorization;

    /**
     * Create a new policy instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * ログインユーザの権限をチェック
     *
     * @param User $user
     * @return Response
     */
    public function allow(User $user): Response
    {
        $route_name = Route::currentRouteName();

        if (!Auth::check()) {
            return Response::deny('権限がありません。');
        }

        $allowed = (bool)Auth::user()->role->authorizations->where('route_name', $route_name)->count();

        if ($allowed) {
            return Response::allow();
        } else {
            return Response::deny('権限がありません。');
        }
    }
}

作ったポリシーをサービスプロバイダで登録します。
このときにGate::define()を使います。

app/Providers/AuthServiceProvider.php
<?php

namespace App\Providers;

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

class AuthServiceProvider extends ServiceProvider
{
    /**
     * The policy mappings for the application.
     *
     * @var array
     */
    protected $policies = [
        // 'App\Model' => 'App\Policies\ModelPolicy',
    ];

    /**
     * Register any authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Gate::define('allow-request-api', 'App\Policies\AllowRequestApi@allow');
    }
}

登録したので、次はそのポリシーをルーティングで使います。
ミドルウェアの'can:allow-request-api'がそれです。不思議な述語になってしまったのは残念ですが。。。

routes/web.php
<?php

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::get('/', function () {
    return view('welcome');
});

Auth::routes();

Route::get('/home', 'HomeController@index')->name('home');

Route::prefix('api')->middleware(['auth', 'can:allow-request-api'])->group(function () {
    Route::prefix('user')->group(function () {
        Route::get('/', 'UserIndex')->name('user_index');
        Route::post('create', 'UserCreate')->name('user_create');
    });
});

APIの説明は省略します。
1ファイル1メソッドのAPI(コントローラ)を作りたい場合は、--invokableオプションで作れます。

$ php artisan make:controller UserIndex --invokable
$ php artisan make:controller UserCreate --invokable

テスト実行

$ vendor/bin/phpunit tests/Feature/UserIndexTest.php 
PHPUnit 8.5.1 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 597 ms, Memory: 24.00 MB

OK (2 tests, 2 assertions)
$ vendor/bin/phpunit tests/Feature/UserCreateTest.php 
PHPUnit 8.5.1 by Sebastian Bergmann and contributors.

..                                                                  2 / 2 (100%)

Time: 188 ms, Memory: 24.00 MB

OK (2 tests, 3 assertions)

成功しました:beer:

まとめ

認可は他にも様々な書き方があります。
今回はルーティングベースの認可でしたが、モデルベースの認可もあります。
奥が深いです。
最高の認可を求める旅はまだまだ終わりません:bike::red_car::sailboat::airplane::rocket:

7
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?