Laravel  権限毎にアクセスできるURLを制限してみた

## はじめに
Laravelのルートファイルを複数に分割して、ユーザー毎にアクセスできるURLを制限してみたのでご紹介します。
Webサービスでは、特定のユーザーのみアクセスを許可したいURLがあったりしますよね。
管理者ページとかはまさに、管理者しかアクセスさせたくないでしょう。
しかし、一般ユーザーが管理者のページにアクセスできるようにしてたとしても気がつかない場合があります。
なので、__*適切に制限をかけることで事故が発生しにくい開発の手法*__として紹介します。

## 環境
```terminal:開発環境
Host:
　　　Mac OS version 11.0.1
     Mac mini(2018)

Docker:
     version 20.10.0

PHP:
    version 7.4.11

Laravel:
     version 6.20.7
```

## ルートファイル
ここでは１つのルートファイルでアクセスできるページを制限するパターンと、複数のファイルで分割する場合を比較します。

#### デフォルトルートファイル（__routes/web.php__）


```php:web.php
// ルートにアクセスした時にwelcomeページが表示される記述です。
// ここでは何も制限していません
Route::get('/', function () {
    return view('welcome');
});

// 認証済のユーザーだけが/homeにアクセスできるように制限している記述です。
Route::group(['middleware' => ['auth']], function(){
    Route::get('/home', 'HomeController@index')->name('home');
});
Auth::routes();
```

これだけ見ると、ファイルを分割するメリットはわからないですよね。
では、次のようなファイルで間違えずに制限をかける自信はありますか？
__*3種類のユーザー権限が存在し、１つのルートファイル内で制限をかける例*__です。

```php:web.php

// ここは誰でもアクセスできるようにしています。
// 記事の閲覧は誰でも可能という場合を想定しています。
Route::get('/article', function () {
    return view('article.index');
});
Route::get('/article/1', function () {
    return view('article.show', compact('1'));
});
Route::get('/article/2', function () {
    return view('article.show', compact('2'));
});
Route::get('/article/3', function () {
    return view('article.show', compact('3'));
});
Route::get('/article/4', function () {
    return view('article.show', compact('4'));
});


// 認証されていないユーザーであれば、コメントを投稿するページへアクセスできないように制限しています。
Route::group(['middleware' => ['auth']], function () {
    Route::get('/article/comment/1', function () {
        return view('article.comment', compact('記事1のコメントページ'));
    });
    Route::get('/article/comment/2', function () {
        return view('article.comment', compact('記事2のコメントページ'));
    });
    Route::get('/article/comment/3', function () {
        return view('article.comment', compact('記事3のコメントページ'));
    });
    Route::get('/article/comment/4', function () {
        return view('article.comment', compact('記事4のコメントページ'));
    });
});

// 管理者以外は記事を削除できないように制限しています。
// 'auth_admin'ミドルウェアの説明はファイルを分割したパターンで紹介しています。
Route::group(['middleware' => ['auth_admin']], function () {
    Route::delete('/article/1', 'ArticleController@delete');
    Route::delete('/article/2', 'ArticleController@delete');
    Route::delete('/article/3', 'ArticleController@delete');
    Route::delete('/article/4', 'ArticleController@delete');
});

Auth::routes();
```

ページやアクションが増えればもっと分かりにくくなります。
ある程度の規模のサイトでは、この数倍以上のコード量になるでしょう。

__*それでは、ファイルを分割した場合を見てみましょう。*__

#### 分割ルートファイル
今回はguest.phpとauth.phpとadmin.phpの３つに分割します。
３つのファイルはweb.phpと同じディレクトリ内に作成します。

```php:guest.php
Route::get('/article', function () {
    return view('article.index');
});
Route::get('/article/1', function () {
    return view('article.show', compact('1'));
});
Route::get('/article/2', function () {
    return view('article.show', compact('2'));
});
Route::get('/article/3', function () {
    return view('article.show', compact('3'));
});
Route::get('/article/4', function () {
    return view('article.show', compact('4'));
});
Auth::routes();
```

```php:auth.php
Route::get('/article/comment/1', function () {
    return view('article.comment', compact('記事1のコメントページ'));
});
Route::get('/article/comment/2', function () {
    return view('article.comment', compact('記事2のコメントページ'));
});
Route::get('/article/comment/3', function () {
    return view('article.comment', compact('記事3のコメントページ'));
});
Route::get('/article/comment/4', function () {
    return view('article.comment', compact('記事4のコメントページ'));
});
```

```php:admin.php
Route::delete('/article/1', 'ArticleController@delete');
Route::delete('/article/2', 'ArticleController@delete');
Route::delete('/article/3', 'ArticleController@delete');
Route::delete('/article/4', 'ArticleController@delete');
```

ファイルを分割することができました。
でも、お気づきでしょうか？
middlewareの記述がなくなっています。
実は、ファイル毎に適用するミドルウェアを設定することができるんですよね。
次のファイルで設定します。

__*app/Providers/RouteServiceProvider.php*__

```php:RouteServiceProvider.php
.
.
.

public function map()
    {
        $this->mapApiRoutes();

        $this->mapWebRoutes();

        // ここで３つのファイルをルートファイルとして登録しています
        // ここより、さらに下の方にそれぞれのルートファイルに対する詳細な設定が可能です
        $this->mapGuestRoutes();

        $this->mapAuthRoutes();

        $this->mapAdminRoutes();
    }

.
.
.

// guest.phpルートファイルの設定です。
protected function mapGuestRoutes()
    {
        // guest.phpの記述には制限を付けないので、middlewareにデフォルトの'web'のみを適用しています
        Route::middleware('web')
            ->namespace($this->namespace)
            ->group(base_path('routes/guest.php'));     // ここでファイルのパスを設定しています
    }

// auth.phpルートファイルの設定です。
protected function mapAuthRoutes()
    {
        // auth.phpでは認証されたユーザーのみアクセスできるようにします。
        // そのため、middlewareに'auth'を記述しています。
        Route::middleware('web', 'auth')
            ->namespace($this->namespace)
            ->group(base_path('routes/auth.php'));
    }

// admin.phpルートファイルの設定です。
protected function mapAdminRoutes()
    {
        // prefixを記述することで、URLの先頭にadmin/を付与します。
        // middlewareには自作した'auth_admin'を適用しています。
        // 'auth_admin'により、認証済、かつ、管理者のみアクセスできるようにします。
        Route::prefix('admin')
            ->middleware('web', 'auth_admin')
            ->namespace($this->namespace)
            ->group(base_path('routes/admin.php'));
    }
```

ルートファイルの設定はこれで終了です。
ちなみに、middleware('auth_admin')は次のファイルで設定されています。

__*app/Http/Kernel.php*__

```php:Kernel.php
.
.
.
protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'auth_admin' => \App\Http\Middleware\AuthAdmin::class, //⬅︎⬅︎⬅︎ここで'auth_admin'という名前と、AuthAdminクラスを紐付けています。
    ];
.
.
.
```


'auth_admin'と紐づいている、AuthAdminクラスの内容は次のようになります。
__*app/Http/Middleware/AuthAdmin.php*__

```php:AuthAdmin.php
<?php

namespace App\Http\Middleware;

use Closure;
use App\Models\Admin;
use Illuminate\Support\Facades\Auth;

class AuthAdmin
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
       
        // 認証済でなければ存在しないページへ移動
        $is_auth = Auth::check();
        if (!$is_auth){
            return abort(404);
        }

        // adminsテーブルに存在するIDのユーザーでなければ、存在しないページへ移動
        $id = Auth::id();
        $is_admin = Admin::where('user_id', $id)->exists();
        if(!$is_admin){ return abort(404); }
        
        return $next($request);
    }
}
```

## おわりに
ルートファイルを分割することで、ユーザー毎にアクセスできるURLを制限しました。
サービスは作って終わりではなく、徐々にスケールアップしていくものです。
そのため、成長段階では多くのプログラマーがファイルを触ることになります。
そんな時、うっかり管理者ページへ誰でもアクセスできるようにしていてはダメですよね。
故意にそんなことする人はいませんので、__*できるだけ事故は仕組みで防ぎましょう*__という内容でした。

最後まで読んでいただきありがとうございます。
この記事があなたのお役に立てれば嬉しいです:smile: