buildしてから気づいたんだけど、思い違いがあれば是非指摘して欲しい。以下の記事では「問題」って書いてるけど、別にこれは問題じゃなくて適正な仕様だと思っているので別にInertia.jsが悪いわけではない。それを意識して使えない方が悪い。…が、初心者とかが何も考えずに「うわー、便利!」って使うとそりゃそうなるよね…っていう気はする。
Laravel+Inertia.jsのセキュリティ上の問題点
RoutingやPageのJavaScriptが露出してしまうこと。Build時にHashが付けられるので、PageのJavaScriptは当てずっぽうではURLを突き止めにくいけど、
Routing見えちゃってるよ問題
Breezeでお手軽に作れるLoginページでChrome DevToolsのコンソールから
Object.entries(Ziggy.routes).forEach((r)=> console.log(r[1].uri))
ってやると、
sanctum/csrf-cookie
/
logout
dashboard
register
login
forgot-password
reset-password/{token}
reset-password
verify-email
verify-email/{id}/{hash}
email/verification-notification
confirm-password
password
storage/{path}
と出る。ということは、あなたのLaravel+Inertia.jsアプリケーションはトップページでこのコンソールコマンドを打たれたら全てのURIが出てしまうということになる。
対応策
ZiggyにRouting情報を渡しているのは、Viewのルートになるapp.blade.phpになる。この@routes ディレクティブには引数を渡せる。例えば、
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title inertia>{{ config('app.name', 'Laravel') }}</title>
@routes('admin')
みたいに。
で、この引数は、config/ziggy.php に紐づけられる。ここで、グループ設定をしてやることで表出するRoutingを制限できる。
<?php
return [
'groups' =>
'admin' => ['admin.*'],
];
色々なやり方ができるので、ZiggyのREADMEを読んで対応する。
resources/js/Pages 配下がアクセスできちゃってるよ問題
StaticなJavaScriptなんてアクセスできてもDBの内容はAPIで保護しとるし問題ないやろ!とか思うかもしれないが、使い方とか微妙にセンシティブな記述があるとあまり気持ちのよろしいものではない。そういう態度が(略
これはどこで出てしまうかというと、build後のHTMLに出てしまっている。ソースコードを見ると
document.head.append(fragment)
})
loadNext(JSON.parse('[{\u0022rel\u0022:\u0022prefetch\u0022,\u0022href\u0022:\u0022https:\\\/\\\/laravel.inertiajs.example.com\\\/build\\\/assets\\\/DeleteModal-sGdPz8-i.js\u0022,\u0022fetchpriority\u0022:\u0022low\u0022},{\u0022rel\u0022:\u0022prefetch\u0022,\u0022href\u0022:\u0022https:\\\/\\\/laravel.inertiajs.example.com\\\/build\\\/assets\\\/InputError-D8SSzIW6.js\u0022,\u0022fetchpriority\u0022:\u0022low\u0022},{\u0022rel\u0022:\u0022prefetch\u0022,\u0022href\u0022:\u0022https:\\\/\\\/laravel.inertiajs.example.com\\\/build\\\/assets\\\/CAlert-CTPOVJDO.js\u0022,\u0022fe
というJavaScriptのPrefetch部分である。これはnpm run devではhmrであるのでbuild後じゃないと見えない。
ではこれはどこで制御するのかというと、viteの入り口である resources/ts/admin.tsx になる。先ほどのbladeファイルのすぐ下の部分で指定されている。
@routes('admin')
@viteReactRefresh
@vite(['resources/ts/admin.tsx', "resources/ts/Pages/{$page['component']}.tsx"])
@inertiaHead
対応策
いくつかの面から対応する必要がある。
そもそも、アクセスされても仕方がないというスコープを設計段階で決めておき、それらをPagesの下階層のフォルダに分割しておく。
こんな感じ。
resources/ts/Pages/
├── Admin(管理者ページ)
├── App(ログイン後のユーザー用ページ)
├── Auth(Breezeの作ったページ)
└── Guest(ゲストユーザー用のページ)
分割したファイルに合ったようにそれぞれのPathを設定する
これが結構な影響がある。以下のような流れで考える。
route => bladeファイル =>Reactの起点&デフォルトページのtsx => LaravelのInerrtia::render()が返すPageのPath
この全てで対応が必要。
Inertia.jsのrootViewを変更してrootになるbladeファイルを変更する。
app/Http/Middleware/HandleInertiaRequests.php がrootViewを決定するのでこんな感じ。
<?php
public function rootView(Request $request)
{
if ($rootView = $this->getRootViewName($request)) {
return $rootView;
}
return parent::rootView($request);
}
private function getRootViewName(Request $request): string | null
{
$routeName = $request->route()->getName();
if (strpos($routeName, 'admin.') === 0) {
return 'admin';
}
if ($routeName === 'home' || strpos($routeName, 'guest.') === 0) {
return 'guest';
}
if (
in_array($routeName, ['login', 'logout', 'register',]) ||
strpos($routeName, 'password.') === 0 ||
strpos($routeName, 'verification.') === 0
) {
return 'auth';
}
return null;
}
}
ここで、routeによって4つのbladeに振り分けることができる。admin.blade.php app.blade.php auth.blade.php guest.blade.php の4つを作っておく。
bladeファイルの記述とLaravel Controllerのメソッドの返り値
先ほどまでで変更してきたものの、 @vite ディレクティブの引数の配列第二要素はアクセスしたURLに対応するデフォルトのPageになる。つまり、あるRouteに対してLaravel Controllerで以下のように返していた場合、
<?php
namespace App\Http\Controllers;
use Inertia\Inertia;
class AdminController extends Controller
{
public function topPage(string $page = ''): \Inertia\Response
{
return Inertia::render('Admin/Index');
}
}
bladeの {$page['component']} には、Admin/Index が入る。Controllerの全てのメソッドの返り値に逐一 Admin/を付けてもいいのだが、それは面倒なので
@routes('admin')
@viteReactRefresh
@vite(['resources/ts/admin.tsx', "resources/ts/Pages/Admin/{$page['component']}.tsx"])
@inertiaHead
としておく。ただし、ControllerではPathが1階層ずれることを忘れないように。
Reactの起点である'resources/ts/admin.tsx' の変更
ここまで来てようやくPrefetchのための記述を変更することができる。 resolvePageComponentの引数2つに /Admin を入れることができる。
createInertiaApp({
title: (title) => `${title} - ${appName}`,
resolve: (name) =>
resolvePageComponent(
`./Pages/Admin/${name}.tsx`,
import.meta.glob('./Pages/Admin/**/*.tsx'),
),
ここまでやってなんだけど
こんなのみんな普通に意識できてるんかな…