はじめに
PHPとLaravelで作られたシステムの保守を担当しています。
先日、Middlewareを使って、ページごとに多段階のアクセス制御を実装する機会がありました。忘備録を兼ねて、その設計と実装方法をまとめます。
Middlewareとroutes/web.phpでアクセス制御の設計が一望できるのはとても便利だなと感じました。
前提
- Laravel 13
- PHP 8.4
(Laravel 11以上, php 8.2以上なら変わらないかと)
目指す構成
今回は、/login、/home、/admin/settings、/admin/secrets の4つのページを持つ業務Webアプリケーションを例にします。
/loginだけはログインフォームを持ち、あとは簡単なメッセージを表示するシンプルな画面を返すだけの最低限のアプリケーションです。
アクセス制御のルールは以下の通りです。
| パス | 認証 | admin権限 | IP制限 |
|---|---|---|---|
| /login | 不要 | - | - |
| /home | 必要 | 不要 | 不要 |
| /admin/settings | 必要 | 必要 | 不要 |
| /admin/secrets | 必要 | 必要 | 必要 |
このように、アクセスするページが深くなるごとに関門が増えていく構成です。
基本的な認証・認可に加え、一番深い関門では指定したIPアドレス(社内ネットワークなど)からでないと通過することができないようにします。
routes/web.phpで見る全体像
Laravelでは、各ルート(あるいはルートのグループ)に対して適用するMiddlewareを routes/web.php で宣言できます。
今回は、以下の3つのMiddlewareを組み合わせて多段階の制御を表現します。
- auth(標準搭載:ログインチェック)
- can:access-admin(標準搭載:権限・認可チェック)
- check-ip(自作:IPアドレス制限)
実際のroutes/web.phpの全体像は以下のようになります。
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\PageController;
use Illuminate\Support\Facades\Route;
// ログイン関連(認証不要)
Route::get('/login', [AuthController::class, 'create'])->name('login');
Route::post('/login', [AuthController::class, 'store'])->name('login.store');
// 1. 認証が必要なグループ(関門①)
Route::middleware('auth')->group(function (): void {
Route::get('/home', [PageController::class, 'home'])->name('home');
// 2. さらに管理者権限が必要なグループ(関門②)
Route::prefix('admin')
->middleware('can:access-admin')
->name('admin.')
->group(function (): void {
Route::get('/settings', [PageController::class, 'adminSettings'])->name('settings');
// 3. さらに特定のIPアドレスからのみアクセス可能なグループ(関門③)
Route::get('/secrets', [PageController::class, 'adminSecrets'])
->middleware('check-ip')
->name('secrets');
});
});
そもそも:LaravelのMiddlewareとは
Middlewareの役割
Middlewareは、一言でいうと 「アプリケーションとクライアントの間に立ち、リクエストやレスポンスの送受信を途中で捕まえてチェックや前処理・後処理を行う仕組み」 です。
かなりいろいろなことができますが、おそらく認証チェックに用いるのが一番ポピュラーなのではないかと思うので、ここではアクセス制御の用途に焦点を当てて考えます。
Middlewareは、アプリケーションに送信されたリクエストがコントローラに到達する前に中身を検査することで、
- ログインしているか?
- 必要な権限を持っているか?
- 許可されたネットワークからのアクセスか?
といった、アプリケーション全体や特定の機能群で「共通して必要になるセキュリティチェック」を、コントローラの外側に切り出して一元管理できます。
各コントローラに毎回同じチェック処理を書く必要がなくなるため、コードの重複を防ぎ、セキュリティの抜け漏れをなくすことができます。
リクエストからレスポンスまでの流れ
普段はweb.phpでルートを定義するだけなので意識しづらいですが、リクエストが送信されてからコントローラが呼ばれるまでは、例えば今回の構成では以下のようなパイプラインを通過しています。
ブラウザ(リクエスト)
│
▼
┌───────────────┐
│ ルーティング │ (どのルートにマッチするか判定)
└───────┬───────┘
│
▼
┌───────────────┐
│ Middleware ① │ [例: auth] (未ログインならここで弾いてリダイレクト)
└───────┬───────┘
│ Passed!
▼
┌───────────────┐
│ Middleware ② │ [例: can:access-admin] (権限がなければここで 403 応答)
└───────┬───────┘
│ Passed!
▼
┌───────────────┐
│ Middleware ③ │ [例: check-ip] (IPが不一致ならここで 403 応答)
└───────┬───────┘
│ Passed!
▼
┌───────────────┐
│ Controller │ (安全が保証された状態でビジネスロジックを実行)
└───────┬───────┘
│
▼
レスポンス
途中のMiddlewareで1つでも条件を満たさなかった場合、処理はそこで中断され、コントローラまでリクエストが届くことはありません。
実装方法
0. 【準備】データベースとモデルの拡張
Laravel標準の認証・認可では、「存在するユーザか?」「どのような権限(ロール)を持っているか?」をDBを見て判定します。
auth Middlewareとcan Middlewareで使えるように、まずはAuthenticatableを継承したEloquentモデルを作成し、そのモデルとマッピングできるテーブルを定義します。
ここではUserモデルとusersテーブルとします。
User モデル
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
protected $fillable = [
'email',
'password',
'role',
];
protected $hidden = [
'password',
];
protected function casts(): array
{
return [
'password' => 'hashed',
];
}
}
users テーブル
mysql> desc users;
+------------+----------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+------------+----------------------+------+-----+---------+----------------+
| id | bigint unsigned | NO | PRI | NULL | auto_increment |
| email | varchar(255) | NO | UNI | NULL | |
| password | varchar(255) | NO | | NULL | |
| role | enum('admin','user') | NO | | user | |
| created_at | timestamp | YES | | NULL | |
| updated_at | timestamp | YES | | NULL | |
+------------+----------------------+------+-----+---------+----------------+
6 rows in set (0.00 sec)
mysql> select * from users;
+----+-------------------+------------------+-------+---------------------+---------------------+
| id | email | password | role | created_at | updated_at |
+----+-------------------+------------------+-------+---------------------+---------------------+
| 1 | user@example.com | $2y$12$XD2Mnu... | user | 2026-07-22 21:52:49 | 2026-07-22 21:52:49 |
| 2 | admin@example.com | $2y$12$/8bGpJ... | admin | 2026-07-22 21:52:49 | 2026-07-22 21:52:49 |
+----+-------------------+------------------+-------+---------------------+---------------------+
2 rows in set (0.00 sec)
LaravelのEloquentモデルは標準でcreate_at,update_atを持つので今回は特にそれを除外せず、最もシンプルにUserモデルを書きました。
emailとハッシュ化したpasswordで認証する、最低限のものを想定しています。
roleは'user'(一般ユーザ権限)と'admin'(管理者権限)の2つとします。
AuthController.phpの実装例
/loginにリクエストした際にルーティングから呼び出されるAuthController.phpでのログイン処理の例です。
<?php
namespace App\Http\Controllers;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\View\View;
class AuthController extends Controller
{
public function create(): View
{
return view('auth.login');
}
public function store(Request $request): RedirectResponse
{
$credentials = $request->validate([
'email' => ['required', 'email'],
'password' => ['required', 'string'],
]);
if (! Auth::attempt($credentials)) {
return back()
->withErrors(['email' => '認証情報が正しくありません。'])
->onlyInput('email');
}
$request->session()->regenerate();
return redirect()->intended('/home');
}
}
Auth::attempt()という部分で、認証に関わる以下の重要な処理を行なっています。
- 入力された
emailをusersテーブルで検索する - 存在していればさらにパスワードが正しいかを照合する
- 認証情報が有効ならログイン状態にし、Sessionに
email(ユーザID)を保存する
このログイン処理以降、リクエストのたびにSessionに保存されたユーザIDを取得し、Userモデルを使って認証・認可のチェックができるようになります。
これで準備が整いました。
1. auth Middleware(認証チェック)
まずは基本となる「ログインしているかどうか」の確認です。
Laravelには、最初からauthという名前のMiddlewareが標準で用意されているため、それがそのまま使えます。
routes/web.phpで次のようにルートを囲むだけで適用できます。
Route::middleware('auth')->group(function (): void {
Route::get('/home', [PageController::class, 'home'])->name('home');
});
auth Middlewareが呼ばれると、内部的にはSessionに保存されているユーザIDをもとにUserモデルを復元し、ログイン済みかどうかを判定しています。
2. can Middleware(認可チェック)
次に、「ログインしているユーザーが、特定の操作をする権限を持っているか(=何ができるか)」をチェックします。これを 認可(Authorization) といいます。
Laravelには、Gate(ゲート)やPolicy(ポリシー)といった認可機能と連動する can Middlewareが標準で用意されています。
Gateの設定(app/Providers/AppServiceProvider.php)
AppServiceProviderのbootメソッドでaccess-adminという名前のGateを定義します。
<?php
namespace App\Providers;
use App\Models\User;
use Illuminate\Support\Facades\Gate;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
public function register(): void
{
//
}
public function boot(): void
{
// ユーザーの role カラムが 'admin' の場合のみ許可
Gate::define('access-admin', fn (User $user): bool => $user->role === 'admin');
}
}
ここでは、Sessionに保存されたユーザIDから復元したUserモデルを見て、roleが指定のものかをチェックしています。
ルートへの適用
定義したGateの名前(access-admin)を、can:Gate名 の形式でMiddlewareに指定します。
Route::prefix('admin')
->middleware('can:access-admin')
->name('admin.')
->group(function (): void {
//略
});
3. check-ip Middleware(自作IPアドレス制限 & 登録)
最後に、IPアドレスによるアクセス制御を実装します。
1) Middlewareの作成
以下のコマンドで、Middlewareの雛形を生成します。
php artisan make:middleware CheckIp
2) Middlewareの実装(app/Http/Middleware/CheckIp.php)
リクエストオブジェクトから接続元IPを取得し、許可リストに含まれているかを判定します。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\IpUtils;
use Symfony\Component\HttpFoundation\Response;
class CheckIp
{
/**
* @param Closure(Request): Response $next
*/
public function handle(Request $request, Closure $next): Response
{
if (! IpUtils::checkIp($request->ip(), [
'127.0.0.1',
'::1',
'192.168.1.0/24'
])
) {
abort(403);
}
return $next($request);
}
}
Symfony\Component\HttpFoundation\IpUtilsライブラリを用いることで、サブネットマスクを使って、許可するIPアドレスを範囲で指定することが可能です。
3) bootstrap/app.phpへのエイリアス登録
自作したMiddlewareをweb.phpで簡単に呼び出せるようにエイリアスを登録します。
bootstrap/app.phpで登録できます。
<?php
use App\Http\Middleware\CheckIp;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Foundation\Configuration\Middleware;
return Application::configure(basePath: dirname(__DIR__))
->withRouting(
web: __DIR__.'/../routes/web.php',
commands: __DIR__.'/../routes/console.php',
health: '/up',
)
->withMiddleware(function (Middleware $middleware): void {
// 自作ミドルウェアのエイリアス登録
$middleware->alias([
'check-ip' => CheckIp::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->shouldRenderJsonWhen(
// ...
);
})->create();
これでcheck-ipというエイリアスでこのMiddlewareを呼び出せるようになりました。
web.phpへの適用は以下のように書きます。
Route::get('/secrets', [PageController::class, 'adminSecrets'])
->middleware('check-ip')
->name('secrets');
余談ですが、Laravel11より前はbootstrap/app.phpではなくapp/Http/Kernel.phpでエイリアス登録を行ってました。
現場で触ってるシステムもLaravel5なのでKernelに書いた覚えがあったのですが、Laravel13でもう一度実装しようとして調べてみて、「そうなんだ!」となった部分です。
この「bootstrap」はcssのライブラリのことではなく、起動時処理みたいな意味の言葉ですね。
動作確認
1.'user'ロールでのアクセス
ログインに成功し、/homeに入れましたが、/admin/*にアクセスしようとすると403が返ってしまいました。期待通りです。
2.'admin'だが許可外のIPアドレスでのアクセス
admin/secretsだけで拒否される挙動を再現するために、ここではCheckIp.phpの許可するIPアドレスリストからループバックアドレスを消しました。(実運用ではそんなことないと思いますが)

/home, /admin/settingsは入れましたが、/admin/secretsでは403が返りました。期待通りですね。
3.'admin'かつ許可されたIPアドレスでのアクセス
おわりに
各コントローラのコンストラクタやメソッドの中でそれぞれ認証・認可チェックを差し込むことも可能ですが、web.phpにMiddlewareとして宣言しておくことで、「どのURLに、どんなアクセス制限がかかっているのか」が一目で把握できるようになり、見通しと保守性が劇的に向上しました。
実際に運用されているシステムだと、もっとたくさんのロールと機能があり、全てのコントローラを見てアクセス制御を確認して回るのは途方に暮れてしまうことも多いと思うので、Middlewareを使った一括の入り口管理を使いこなしていきたいです。

