この記事ではLaravelのミドルウェアでIP制限を実装する方法を紹介します。
先日、業務でIP制限の実装タスクを担当しました。
わからないことだらけだったので調べながら進めて行ったのですが、学びが多かったので共有したいと思います。
特に、AWS環境などでCloudFrontやロードバランサーを利用している場合にハマったこともあったのでそれについても詳しく解説したいと思います。
LaravelではIP制限の機能を楽に実装できるのでぜひ参考にしてみてください。
この記事を読むメリット
・LaravelでのIP制限機能の実装の流れが分かる
・CloudFrontやロードバランサーを利用している際にハマるポイントと対処方法が理解できる
ここから実際のIP制限の実装方法を紹介していきます。
テーブル構成の前提条件
今回の前提条件は以下として進めていきます。
「usersテーブル」と「user_authorized_ipsテーブル」を使用します。
ユーザーは複数のIPアドレスを登録することがあることを想定しhasManyの関係にしています。
ちなみに上画像のER図はNotionのMermaidで作成しています。
簡単に作成できるので気になる方は以下記事を参考にしてみてください。
LaravelのミドルウェアでIP制限を実装する方法
ここから実際のIP制限の実装方法を紹介します。
大きな流れはこちらになります。
- ミドルウェアファイルを作成
- IP制限の処理を追加
- CloudFrontやロードバランサーを使用している場合の処理を追加
- カーネルにミドルウェアを登録
- IP制限が必要なルーティングにミドルウェアを追加
1つずつ紹介していきます。
ミドルウェアファイルを作成
まずはミドルウェアファイルを作成します。
以下artisanコマンドを実行します。
php artisan make:middleware CheckIp
「CheckIp」部分は自由に指定して良いです。
すると、「src/app/Http/Middleware/CheckIp.php」が作成されます。
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
class CheckIp
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next)
{
return $next($request);
}
}
「public function handle(Request $request, Closure $next)」の中身に実際のIP制限処理を書いていきます。
エンジニアにおすすめ書籍
エンジニアになりたて、これから勉強を深めていきたいという方におすすめの書籍はこちら!
IP制限の処理を追加
続いてはCheckIp.phpの中身にIP制限の処理を追加していきます。
public function handle(Request $request, Closure $next)
{
$user = Auth::user();
$ipAddress = $request->ip();
// ログインユーザーに許可されているIPアドレスを取得
$authorizedIpAdresses = $user->userAuthorizedIpAddresses->pluck('ip_address')->all();
// 登録IPアドレスが1つもなかったらそのままログインさせる
if (empty($authorizedIpAdresses)) {
return $next($request);
}
// アクセス元のIPアドレスとログインユーザーが許可されているIPアドレスを比較して登録されていなかったら403ページへ
if (!IpUtils::checkIp($ipAddress, $authorizedIpAdresses)) {
Log::error('App\Http\Middleware\CheckIpAddress: Access denied for IP. User ID >>> '. $user->id. ' / Request IP >>> '. $ipAddress);
abort(403);
}
return $next($request);
}
ユーザー、アクセスIPアドレスを取得
まずは「$user = Auth::user();」でログインユーザーを取得します。
続いて「$ipAddress = $request->ip();」でアクセス元のIPアドレスを取得します。
「$request->ip()」で取得できます。
しかしこのやり方ではCloudFrontやロードバランサーを利用している場合に、経由しているIPアドレスが返ってきてしまいます。
私はこれでハマってしまいました。
後ほどその対応策も紹介します。すぐに確認したい方はこちらから。
ログインユーザーに許可されているIPアドレスを取得
ユーザーモデルからそのユーザーに登録しているIPアドレスを取得します。
「pluck(‘ip_address’)」とすることでip_addressのコレクションデータを取得しています。
IPアドレスのチェック処理
メインのIPアドレスチェックですが、Laravelが用意している関数を利用します。
使うのは「IpUtils::checkIp」です。
「IpUtils::checkIp(アクセス元IPアドレス, ユーザーに登録しているIPアドレス)」とすることでチェックを行なってくれます。
サブネットマスクの場合も含めたチェックを行なってくれるのでめちゃ便利です。
なので返り値がfalseだった場合に、エラーログを出して403ページに遷移するようにします。
CloudFrontやロードバランサーを使用している場合の処理を追加
続いて先ほども触れたCloudFrontやロードバランサーを使用している場合の処理を追加していきます。
まずは「src/app/Http/Middleware/TrustProxies.php」でプロキシの設定を行います。
ファイル内の$proxiesに信頼するプロキシを設定します。
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
AWSを利用している場合、実際のバランサーのIPアドレスがわからない場合があるため以下のように設定すると良いそうです。
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies = '*';
公式にも解説がありました。
https://readouble.com/laravel/8.x/ja/requests.html
本番環境ではELBを利用しておりアクセス元IPアドレスを取得できなかった
開発環境の際に問題なくアクセス元のIPアドレスが取得できていたので満足していました。
しかし、本番環境ではELBを利用しており意図したIPアドレスを取得できませんでした。
これを解決するために「CheckIp.php」で以下のような修正を行いました。
public function handle(Request $request, Closure $next)
{
$user = Auth::user();
// 本番環境とそれ以外の環境でIPアドレスの取得方法を分ける
if (app()->isProduction()) {
$ipAddress = explode(',', $request->header('X-Forwarded-For'))[0];
} else {
$ipAddress = $request->ip();
}
$authorizedIpAdresses = $user->userAuthorizedIpAddresses->pluck('ip_address')->all();
if (empty($authorizedIpAdresses)) {
return $next($request);
}
if (!IpUtils::checkIp($ipAddress, $authorizedIpAdresses)) {
Log::error('App\Http\Middleware\CheckIpAddress: Access denied for IP. User ID >>> '. $user->id. ' / Request IP >>> '. $ipAddress);
abort(403);
}
return $next($request);
}
本番環境とそれ以外の環境でIPアドレスの取得方法を分ける
アクセス元IPアドレスを格納する変数「$ipAddress」の取得方法を本番環境とそれ以外で分けました。
まず「if (app()->isProduction())」で本番環境かどうかを判別します。
Laravelの環境判別方法については以下記事を参考にしてみてください。
本番環境以外では今まで通り「$request->ip()」で取得しています。
本番環境の場合、「X-Forwarded-For」を使うと、ロードバランサーを経由して接続したクライアントの送信元IPアドレスを取得できます。
「X-Forwarded-For」についてはこちら。
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Forwarded-For
これでELBを使った際も正常にアクセス元IPアドレスを取得できるようになりました。
カーネルにミドルウェアを登録
ミドルウェアの処理が完了できたのでカーネルにミドルウェアを登録します。
「src/app/Http/Kernel.php」内の$routeMiddlewareに作成したミドルウェアを追加します。
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::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,
// IP制限チェックミドルウェア追加
'checkIpAddress' => \App\Http\Middleware\CheckIp::class,
];
IP制限が必要なルーティングにミドルウェアを追加
最後はルーティングにミドルウェアを設定します。
IP制限が必要なルーティングのみに設定する必要があります。
Route::middleware(['auth', 'checkIpAddress'])->group(function () {
Route::name('dashboard')
->get('/dashboard', function () {
return view('dashboard');
});
});
今回はログイン領域に必要なミドルウェアだったので「auth」の処理がある箇所に追加しました。
これでログインが必要なページでIP制限のチェック処理が動くようになります。
IP制限実装時ハマりポイントだけ押さえておけばOK
いかがだったでしょうか。
IP制限の実装自体はLaravelが用意してくれている便利なものを利用すれば比較的簡単に実装ができるかと思います。
注意が必要なのはAWSなどのロードバランサーを利用している場合だと思います。
開発環境と本番環境で異なる場合が多いと思いますのでこの記事を参考にしていただければと思います。
参考記事:
https://r-tech14.com/ip_restrictions/
https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/X-Forwarded-For