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エラーが返ってくれば成功というわけです![]()
<?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);
}
}
<?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
<?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()を使います。
<?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'がそれです。不思議な述語になってしまったのは残念ですが。。。
<?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)
成功しました![]()
まとめ
認可は他にも様々な書き方があります。
今回はルーティングベースの認可でしたが、モデルベースの認可もあります。
奥が深いです。
最高の認可を求める旅はまだまだ終わりません![]()
![]()
![]()
![]()
![]()