Inertia.js使ってReactで管理画面の開発してると、「CSRFとかXSSとかどないやねん?」って感じになる。Sanctumを使ったSPA認証ってのがあるみたいだ。
2つ認証にたいするアプローチがあって、
というもの。前者はコマンドラインからAPIを叩くときに使って、後者はブラウザから使う。「ログインしてるんだから、いーじゃん。」と思ったら、やっぱりそうだった。どうも従来のログインを使わないでSPAでログインしたりしたい時に使うっぽい。だから、ログインの実装には別のパッケージが必要で、ログインを実装するには、「手動で実装するか、Laravel Fortify のようなヘッドレスの認証パッケージを使ってください」と公式ページにもある。Qiitaに実装した人の記事あるけど、めんどくさそうですね。
ということで、セッションログインでいいじゃんって。
セッションAPIにする
ルーティング
middleware('auth')をルーティングにつける。
<?php Route::apiResource('users', UserController::class)->middleware('auth');
bootstrap/app.php
のwithMiddlewareで、->api(append:[])
する。
ミドルウェアの登録
<?php ->withMiddleware(function (Middleware $middleware) { $middleware->web(append: [ \App\Http\Middleware\HandleInertiaRequests::class, \Illuminate\Http\Middleware\AddLinkHeadersForPreloadedAssets::class, ])->api(append: [ \Illuminate\Cookie\Middleware\EncryptCookies::class, ]);
vendor/laravel/framework/src/Illuminate/Foundation/Configuration/Middleware.php
を見ると、
\Illuminate\Cookie\Middleware\EncryptCookies::class, \Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class, \Illuminate\Session\Middleware\StartSession::class, \Illuminate\View\Middleware\ShareErrorsFromSession::class, \Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class, \Illuminate\Routing\Middleware\SubstituteBindings::class,
の6つがwebミドルウェアグループに登録してあったけど、1つだけでなんとかなった。
あんまり簡単だったんで、Sanctumを導入して「あるべきAPI」の形にしてみる。
サーバ(Laravel側)の設定
Configuring Your First-Party Domains
ドメインを一致させておけと。当たり前ですね。
Sanctum Middleware
ミドルウェアを変更する。 $middleware->statefulApi(); を追加。
<?php ->withMiddleware(function (Middleware $middleware) { $middleware->statefulApi();
CORS and Cookies
別ドメインじゃないからこれもパス。
ルーティング
こんな感じでmiddleware('auth:sanctum')をルーティングにつける。
<?php Route::apiResource('users', UserController::class)->middleware('auth:sanctum');
クライアント(React側)の設定
ログイン後に、TOKENをもらってCOOKIEに入れておく。…というより、Set-Cookieヘッダによりリクエスト送ったら自動でCOOKIEがセットされるので、そのCOOKIEをHTTPヘッダに乗せてやることでAuthorizationが行われるということ。
TOKENをもらうフェーズ
http://localhost:8000/sanctum/csrf-cookie
にGETでfetchを飛ばすとTokenを発行してくれる。XSRF-TOKENという名前でCOOKIEがセットされているはず。
自分の場合、Inertia.jsで管理画面を作ってるこのコードだと、resources/js/Pages/Admin/Default.tsx
がデフォルトラッパーになるのでここでAjaxを飛ばして取得する。飛ばしさえすればあとはSet-Cookieレスポンスヘッダが返ってくるだけなので、何もしなくても大丈夫。Baseloadingは適当なローダー。
import AuthenticatedLayout from '@/Layouts/AuthenticatedLayout'; import { GlobalConstantsProvider, GlobalConstantsType } from '@/Contexts/GlobalConstants'; import { Head } from '@inertiajs/react'; import BaseLoading from '@/Components/Baseloading'; import { useState } from 'react'; const Default = ({ admin_path }: { admin_path: string; }) => { const [loading, setLoading] = useState<boolean>(true); (async () => { await fetch(route('sanctum.csrf-cookie')) setLoading(false) })() const globalConstants: GlobalConstantsType = { ADMIN_PATH: admin_path } const wrapperStyle = { width: "100%", height: "100%", overflow: "hidden", } return ( <GlobalConstantsProvider defaultValue={globalConstants}> {loading ? <BaseLoading active={true} wrapper={wrapperStyle} fadeSpeed={100}></BaseLoading> : <> <Head title="CoreUI Adminstration Panel Sample" /> <AuthenticatedLayout /> </> } </GlobalConstantsProvider> ); } export default Default
TOKENを使うフェーズ
RequestヘッダにX-XSRF-TOKENを含める。その値はCOOKIEにあるXSRF-TOKEN。axiosを使ってインターセプトしてやるとよい。
axios.interceptors.request.use(config => { config.headers!['X-XSRF-TOKEN'] = cookies['XSRF-TOKEN'] console.log(cookies['XSRF-TOKEN']); return config; }, error => { return Promise.reject(error) })
…が、うまくいかん。401エラーになってしまう…なんで?