Laravelでログインしたユーザーのみ読み取れる画像・アセットを設定する方法
技術スタック LaravelPHP

Laravelでログインしたユーザーのみ読み取れる画像・アセットを設定する方法

2022.03.26

こんにちはjunです。Laravel製のシステムにてWebマニュアルを作成していた時、「あれ?マニュルはログインユーザーのみ見れる様にするけど、画像などはどうすればいいんだ?」という事態がありました。

Laravelはルートを定義し、その際に認証を設けることができます。ただし静的な画像(今回の様なあらかじめセットしておくマニュアル画像など)を配置する場合はpublic配下または、storage/public配下に置くことが多いと思います。しかしそれらのディレクトリは名の通りいかなるアクセスに対してリクエストを許可しています。

そのため

  • ログインをしないと見れない画像やアセット
  • アクセスを制限したい画像やアセット

を実装したい時は単純にpublic配下に置くことはできません。この場合Laravelではコントローラーを使用して、アセットのリクエストに対して一度認証のロジックをかける必要があります。普段Laravelを使用していると、特定のURLとそのビューに対する認証はルートを定義するだけで簡単に設定できます。しかしビュー以外のアセットファイルの場合はWebサーバーとLaravelの仕組みを少し理解している必要ががります。今回はその様な保護したアセットルートの設定方法を解説しようと思います。

概要

Laravelで構築されたURLで指定のビューやファイルをレスポンスとして返す時2通りの処理方法があります。

  1. ドキュメントルート配下にリクエストで示されたファイルがある場合、それを返す。(webサーバー)
  2. ドキュメントルートにない場合、Route.phpで定義したルートを照らし合わせ、設定したビューやファイルを返す。(webサーバー+PHP)

「2」の方はよくわかると思います。例えば以下の様なルートを定義した時

route.php
Route::get('/test', function () {
    return view('welcome');
});

https://example.com/testというURLにアクセスするとview('welcome')で定義したHTML(画面)がレスポンスとして表示されます。一方「1」の方はというと例えばpublic配下に置いたcss,jsなど静的なファイルがあげられます。例えばhttps://example.com/css/style.cssの場合、webサーバーはpublic/css/style.cssがあればそれをレスポンスとして返します。

両者の違いはwebサーバーだけで完結しているか、PHP(Laravel)も動かしているかです。これはpublic配下の.htaccessを見ると理解できます。

.htaccess
<IfModule mod_rewrite.c>
    # Send Requests To Front Controller...
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteRule ^ index.php [L]
</IfModule>

一部省略していますが、重要なのはこの箇所です。これは「もし、リクエストしたディレクトリおよびファイルがない場合、index.phpを実行する」という意味です。つまりLaravelが置かれたwebサーバーでは、まず「リクエストされたファイルが静的に置かれているかをチェック」そしてもしない場合は「index.phpを実行してLaravelが動的にルートに対するレスポンスを作成する」ということが行われています。

Apache側で静的ファイルに対するアクセスを設定していない場合、基本的にリクエストに合致するファイルがある場合はレスポンスしてしまいます。今回の様な保護したファイル、つまりLaravelの認証などを通してファイルを返すためには、独自のルートを定義してレスポンスする必要があります。

Laravelではアップロードされたファイルはstorage/app/public 配下に配置し、そのstorageファイルへのリクエストは上記のwebサーバーのみで処理できます。storageディレクトリがpublicとは別なのになぜできるのか?それはシンボリックリンクを張っているからです。構築時にphp artisan storage:linkというおまじないを唱えたと思います。これはpublic配下にstorage/app/publicに連絡するstorageというシンボリックリンクを配置する処理を行っています。

実際にpublic配下にstorageというものがあり、Vscodeでは矢印マークが加わっているのが分かります。ls -lを実行してみるとstorage -> /var/www/html/storage/app/publicという風に表示されます(私の環境の場合)。ディレクトリとして離れていても、シンボリックリンクを貼ることでstorage/app/public配下をwebサーバーが走査することができる様にしています。

今回のような保護したアセットファイルルートを設定するために

  1. 専用のディレクトリを作成
  2. 読み取りルートの定義
  3. ファイルの取得処理
  4. レスポンス処理

上記のプログラムを作成します。今回は「ログインしたユーザーが見れるwebマニュアルの画像」ということなので、resources配下にファイルを置いておくことにします。一応後でstorageディレクトリに保護ファイルを配置・取得する方法も記述します。

ディレクトリの作成

まずは専用のディレクトリを作成します。今回は静的に置いておくのでresources/protectedという保護アセットファイルディレクトリを作っておきます。リクエストがあった場合はこのディレクトリからファイルを取得します。

ルートを定義する

それではルートを定義します。routes/web.phpにて以下の様なルートを設定。

routes/web.php
Route::group(['middleware' => 'auth'], function () {
    Route::get('/protected/{path?}', function (Request $request,$path='') {
        // 後で書きます..
    })->where('path', '.*');
});

authミドルウェアでグルーピングをしてprotected配下のルートを保護します。

{path?}は任意の記述を意味します。つまり/protected/secret.jpg,/protected/manual/private.pngなどのルートにをキャッチすることができます。そして{path?}パラメータはコールバック(コントローラー)に第二引数として使用できます。先程の例のパスの場合、$pathsecret.jpg,manual/private.pngとなります。この値は後でファイルの取得に使用します。ちなみに今回はルートに処理内容を記述しますが、プロジェクトによっては複雑な認証処理を実装する場合はコントローラーに記述しても大丈夫です。

ファイルの取得とレスポンスを行う

ではファイルの取得の処理を記述します。

routes/web.php
use Illuminate\Support\Facades\File;

Route::group(['middleware' => 'auth'], function () {
    Route::get('/protected/{path?}', function (Request $request,$path='') {
        if($path==='') abort(404);

        $rp = resource_path('protected/'.$path);
        if(File::exists($rp)){
            return response()->file($rp);
        }else{
            abort(404);
        }
    })->where('path', '.*');
});

最初に$pathがない場合は404にアボートします。そして何かしらファイルが指定された場合はresource_path('protected/'.$path)にてresources/protected配下のファイルパスを取得します。

webサーバーでなくPHP(Laravel)にてユーザーからの入力値(リクエストパス)を用いてファイルの取得をする場合、PHPのfile_get_contents()は使用せずLaravelのresource_path()storage_pathを使用し、さらにFileファサード、file()メソッドを使用しましょう。file_get_contents()../といった記述は文字列でなく、パスとして認識してしまい想定しないディレクトリのファイルにがブラウザを通じて取得される可能性があります。このような脆弱性をディレクトリトラバーサルといいます。Laravelのファイル取得系のメソッドはその辺は対策済みなので、基本的にはLaravelのメソッドを使用しましょう。

ファイルパスを作成したらFile::exists() を使用してファイルが存在するかをチェックします。存在しないファイルをfile()パスで使用するとFileNotFoundExceptionが発生してしまいます。例外処理でやってもいいのですが、responseメソッドを呼んでいるので念のためあらかじめチェックしておきます。

ファイルがある場合は response()->file();を使用して対象のresources/protected配下のファイルをレスポンスとして返します。ない場合は404へアボートします。file()メソッドを使用することで拡張子からcontent-typeを設定してくれるそうでCSVだろうがHTMLでもMP4でも問題なくレスポンスしてくれます。

ルート自体はLaravelのミドルウェアを使用することでファイルを保護し、任意のファイルパスを使用して認証が通ればファイルを取得することができる様になります。

storageでやる方法

resourcesは基本的に開発者が静的にファイルを置く場合に使用します。ユーザーが自由にアップロードして、保護しながら呼び出したい時はstorageディレクトリを使用します。ファイルの取得と保護は上記とほぼ同じですが、storageの場合は少しcconfigの設定を行います。

config/filesystem.phpにて以下の様に保護storageディレクトリを定義します。

config/filesystem.php
'protected' => [
    'driver' => 'local',
    'root' => storage_path('app/protected'),
    'url' => env('APP_URL') . '/storage',
    'visibility' => 'private',
],

こうすることでStorage::disk('protected')->path()を用いて対象ファイルパスを取得することができる様になります。ファイルストレージはローカルでなくS3など外部のものを使用することもあるので、この様に設定ファイルで定義しておくといいです。storageにprotectedディレクトリを作成した後、あとはルートを定義してStorage::disk('protected')->path()を用いてリクエストされたファイルパスを取得し、存在チェックをしてレスポンスで返せばOKです。

今回は簡単なauthミドルウェアですが、権限のロジックを組み込むことで所有者のリクエストのみに見せたり、特定の人のみに見せるといった芸当ができそうです。ただしwebサーバーの静的な配信でなく、ファイルの取得にPHPを動かすことになるので大量配信の場合はパフォーマンスはちょっと心配かもしません。

Copyright © 2021 jun. All rights reserved.