Laravelのmiddleware作成方法[Sanctum]
こんにちは、技術部の Yです。今回はLaravelのmiddlewareの作成方法について書いていきます。
前提
- Sanctumを使用したSPA認証の実装方法に関して書いています。
- API側の実装がメインです。
- APIとフロントは異なるサブドメインで配置されています。
- APIトークン認証に関しては取り扱っていません。
事前準備
今回は同一レポジトリ内にAPI, フロントを配置してます。
トップ
– api (localhost:80)
– front (localhost:3000)
API
Sail を使って環境構築
インストール
curl -s “https://laravel.build/api” | bash
下記のコンテナは今回使用しないため、docker-compose.ymlから削除しました。
redis, meilisearch, mailhog, selenium
Laravel9ではデフォルトでSanctumが入っていたため別途インストールは不要でした。
// composer.json
{
“name”: “laravel/laravel”,
“type”: “project”,
“description”: “The Laravel Framework.“,
“keywords”: [“framework”, “laravel”],
“license”: “MIT”,
“require”: {
“php”: “^8.0.2",
“guzzlehttp/guzzle”: “^7.2",
“laravel/framework”: “^9.19",
“laravel/sanctum”: “^3.0",
“laravel/tinker”: “^2.7"
},
// 省略
環境を立ち上げてルートを確認します。
cd api && sail up
sail artisan route:list
以下のルーティングが確認できるはずです。
GET|HEAD sanctum/csrf-cookie
User一覧取得処理
今回下記のような仕様にして、認証処理を確認したいと思います。
– 認証前はUser一覧を取得できない
– 認証後はUser一覧を取得できる
そのためUserデータの作成と、User一覧取得処理を準備しておきます。
// UserSeeder.php
insert([
[
‘name’ => ‘user’,
‘email’ => ‘user@example.com’,
‘password’ => Hash::make(‘password’),
],
[
‘name’ => ‘user2’,
‘email’ => ‘user2@example.com’,
‘password’ => Hash::make(‘password’),
],
[
‘name’ => ‘user3’,
‘email’ => ‘user3@example.com’,
‘password’ => Hash::make(‘password’),
],
[
‘name’ => ‘user4’,
‘email’ => ‘user4@example.com’,
‘password’ => Hash::make(‘password’),
],
[
‘name’ => ‘user5’,
‘email’ => ‘user5@example.com’,
‘password’ => Hash::make(‘password’),
],
]);
}
}
// DatabaseSeeder.php
call([
UserSeeder::class,
]);
}
}
sail artisan migrate --seed
ルーティング作成
// api.php
Route::get(‘users’, function () {
return User::all();
});
curl ‘http://localhost/api/users’ | jq
フロント
npx create-react-app front
// App.js
import { useState } from ‘react’;
import axios from ‘axios’;
import ‘./App.css’;
function App() {
const [email, setEmail] = useState(‘’);
const [password, setPassword] = useState(‘’);
const [users, setUsers] = useState([]);
const login = () => {}
const logout = () => {}
const getUsers = () => {
http.get(’http://localhost/api/users').then((res) => {
setUsers(res.data);
})
}
const reset = () => {setUsers([])}
const onChangeEmail = (e) => setEmail(e.target.value);
const onChangePassword = (e) => setPassword(e.target.value);
return (
{
users.map((user) => {
return (
{user.name}
)
})
}
);
}
export default App;
export default App;
SPA認証
ドキュメントを参考に進めていきます。
ファーストパーティドメインの設定
「ステートフル」な認証を維持するドメインを指定します。
sanctum.phpのstatefulに設定内容が書かれています。
// sanctum.php
‘stateful’ => explode(‘,’, env(‘SANCTUM_STATEFUL_DOMAINS’, sprintf(
‘%s%s’,
‘localhost,localhost:3000,127.0.0.1,127.0.0.1:8000,::1’,
Sanctum::currentApplicationUrlWithPort()
))),
今回開発環境のフロントはlocalhost:3000で動作しているため、特に変更しませんが、
ドメインを設定している場合や本番環境では、.envのSANCTUM_STATEFUL_DOMAINSに設定が必要です。
Sanctumミドルウェアの設定
ミドルウェアの設定です。
EnsureFrontendRequestsAreStatefulを有効にします。
// diff Kernel.php
‘api’ => [
- // \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
+ \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
‘throttle:api’,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
// EnsureFrontendRequestsAreStateful.php
public function handle($request, $next)
{
$this->configureSecureCookieSessions();
return (new Pipeline(app()))->send($request)->through(static::fromFrontend($request) ? [
function ($request, $next) {
$request->attributes->set(‘sanctum’, true);
return $next($request);
},
config(‘sanctum.middleware.encrypt_cookies’, \Illuminate\Cookie\Middleware\EncryptCookies::class),
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
config(‘sanctum.middleware.verify_csrf_token’, \Illuminate\Foundation\Http\Middleware\VerifyCsrfToken::class),
] : [])->then(function ($request) use ($next) {
return $next($request);
});
}
CORSとクッキー
今回はフロントとAPIのドメインが異なるため、corsの設定が必要です。
この値をtrueにしてあげないと、異なるドメインからのリクエスト時にエラーになってしまいます。
// cors.php
‘supports_credentials’ => true,
ルート保護
ここで、「認証後にのみ、User一覧を取得できる」という仕様を実装したいと思います。
auth:sanctumというミドルウェアが用意されていますので、ルートに対して適用します。
// diff api.php
-Route::get(‘users’, function () {
+Route::middleware(‘auth:sanctum’)->get(‘users’, function () {
return User::all();
});
認証
ログイン、ログアウト処理の実装(API)
簡単なログインログアウトの処理を実装します。
// LoginController.php
validate([
‘email’ => [‘required’, ‘email’],
‘password’ => [‘required’],
]);
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return response()->json(Auth::user());
}
return response()->json([], 401);
}
/**
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return response()->json(true);
}
}
// diff api.php
+use App\Http\Controllers\LoginController;
+Route::post(‘login’, [LoginController::class, ‘login’]);
+Route::post(‘logout’, [LoginController::class, ‘logout’]);
CSRF保護の初期化
フロント側です。
axiosのインスタンスを作成しておきます。
withCredentials: trueと設定することで、後述のXSRF-TOKENをリクエスト時に送信してくれます。
// diff App.js
+ const http = axios.create({
+ baseURL: ‘http://localhost’,
+ withCredentials: true,
+ });
+
認証処理の前に、/sanctum/csrf-cookieに対してリクエストして、アプリケーションのCSRF保護を初期化する必要があります。
const login = () => {
axios.get(‘/sanctum/csrf-cookie’).then((res) => {
// ログイン処理
})
}
レスポンスの内容を確認すると、XSRF-TOKENがSet-Cookieに設定されていることがわかります。
ログイン処理(フロント)
// diff App.js
+ const login = () => {
+ http.get(‘/sanctum/csrf-cookie’).then((res) => {
+ http.post(‘/api/login’, {email, password}).then((res) => {
+ console.log(res);
+ })
+ })
+ }
- const login = () => {}
ブラウザでemail, passwordを正しく入力し、「ログイン」ボタンを押してみます。
ステータス200で返ってきているので認証に成功していそうです。
ログアウト処理(フロント)
diff App.js
+ const logout = () => {
+ http.post(‘/api/logout’).then((res) => {
+ console.log(res);
+ })
}
- const logout = () => {}
トラブルシューティング
axiosがXSRF-TOKENを設定してくれない
/sanctum/csrf-cookieのレスポンスで返ってきたXSRF-TOKENを次回リクエスト時に設定してくれないとCSRF token mismatch.と419エラーになってしまいます。
もともとaxiosのpostメソッドの第3引数にwithCredentials: trueを指定して動かしてましたが、XSRF-TOKENを設定してくれませんでした。
axios.createでインスタンス作成時に指定してあげることで解決できました。
(きちんとフロントエンドを実装する場合、1つ1つのpostメソッドに対してwithCredentialsを設定することはないと思いますので、本来詰まるところではないのかもしれないですが。。)
セッションクッキードメイン設定
// session.php
‘domain’ => env(‘SESSION_DOMAIN’),
// .env
SESSION_DOMAIN=.domain.com
感想
関連記事
- 2022-12-05
- テクノロジー