Laravel8 + Sanctum + ReactでSPA認証
前提
Larave8 + ReactでSPA環境ができていること
この記事ではブログ内で作った続きでやります。 nochio12.hatenablog.com
基本的に公式通り進めます。
laravel.com
Sanctumではトークンでの認証とCookieを使った認証のどちらかを選ぶことができますが、
今回はCookieを使った認証を実装します。
インストール
$ composer require laravel/sanctum $ php artisan vendor:publish --provider="Laravel\Sanctum\SanctumServiceProvider" $ php artisan migrate
2つめのコマンドで設定ファイルとmigrationが作成されます。
ミドルウェアの追加
[Kernel.php]
+ use Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful; 'api' => [ + EnsureFrontendRequestsAreStateful::class, 'throttle:60,1', \Illuminate\Routing\Middleware\SubstituteBindings::class, ],
モデルの修正
[Models/User.php]
+ use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { - use HasApiTokens, Notifiable; + use HasFactory, HasApiTokens, Notifiable; }
CORS対策
[config/cors.php]
- 'paths' => ['api/*], + 'paths' => ['api/*', 'sanctum/csrf-cookie'], - 'supports_credentials' => false, + 'supports_credentials' => true,
実際の環境のドメインをenvに追記
[.env]
+ SANCTUM_STATEFUL_DOMAINS=127.0.0.1:8080
[config/sanctum.php]
'stateful' => explode(',', env('SANCTUM_STATEFUL_DOMAINS', 'localhost,127.0.0.1,127.0.0.1:8000,::1')),
ローカルでの開発で、ここにあるドメインであれば追記しなくて平気ですが
ポートを変えていたり、本番環境ではどっちみち必要。
調べると出てきますが、公式で言及がないため忘れそう。
認証処理
Api用の認証コントローラを作成
$ php artisan make:controller Api/Auth/LoginController
[Api/Auth/LoginController.php]
<?php namespace App\Http\Controllers\Api\Auth; use App\Http\Controllers\Controller; use Illuminate\Foundation\Auth\AuthenticatesUsers; use Illuminate\Http\Request; use Illuminate\Support\Facades\Auth; class LoginController extends Controller { use AuthenticatesUsers; /** * ログイン * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse * * @throws \Illuminate\Validation\ValidationException */ public function login(Request $request) { // バリデーション $this->validateLogin($request); $result = false; $status = 401; $message = 'ユーザが見つかりません'; $user = null; $credentials = $request->only('email', 'password'); if (Auth::attempt($credentials)) { // Success $result = true; $status = 200; $message = 'OK'; $user = Auth::user(); // ※古いトークン削除&新しいトークン生成 $user->tokens()->where('name', 'token-name')->delete(); $token = $user->createToken('token-name')->plainTextToken; } return response()->json(['result' => $result, 'status' => $status, 'user' => $user, 'message' => $message]); } /** * ログアウト * * @param \Illuminate\Http\Request $request * @return \Illuminate\Http\Response|\Illuminate\Http\JsonResponse */ public function logout(Request $request) { Auth::logout(); $result = true; $status = 200; $message = 'ログアウトしました'; return response()->json(['result' => $result, 'status' => $status, 'message' => $message]); } }
フロントの実装
[js/bootstrap.js]
window.axios = require('axios');
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
+ window.axios.defaults.withCredentials = true;
ルーティングの追加
[app.js]
require('./bootstrap'); import React from "react"; import ReactDOM from "react-dom"; import { BrowserRouter, Route } from "react-router-dom"; import NavBar from "./NavBar"; import Home from "./Home"; import About from "./About"; import Auth from "./Auth"; // 追加 const App = () => { return ( <BrowserRouter> <NavBar /> <Route path="/about" component={About} /> <Route path="/auth" component={Auth} /> // 追加 <Route exact path="/" component={Home} /> </BrowserRouter> ); }; if (document.getElementById("app")) { ReactDOM.render(<App />, document.getElementById("app")); }
ログイン画面
[Auth.js]
import React, { useState, useEffect } from "react"; const Auth = () => { const [user, setUser] = useState(null); const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); // ブラウザリロード時にログイン済みか判定 useEffect(() => { getUser(); }, [getUser]); // 認証ユーザを取得 const getUser = () => { axios.get("/api/user").then(res => { console.log('[getUser]ログイン済み'); console.log(res.data); setUser(res.data); }).catch(err => { console.log('[getUser]ログインしてません'); }) }; // ログイン const login = async e => { e.preventDefault(); // ログイン時にCSRFトークンを初期化 axios.get("/sanctum/csrf-cookie").then(response => { axios .post("/api/login", { email, password }) .then(res => { console.log(res.data); if (res.data.result) { console.log('[login]ログイン成功'); setUser(res.data.user); } else { console.log(res.data.message); console.log('[login]ログイン失敗'); } }) .catch(err => { console.log(err.response); console.log('[login]ログイン失敗'); }); }) }; // ログアウト const logout = () => { axios .get("/api/logout") .then(res => { setUser(null); }) .catch(err => { console.log(err); }); }; // ログインフォーム let form = ( <form onSubmit={login}> <label>email</label> <input type="email" value={email} onChange={e => setEmail(e.target.value)} /> <label>password</label> <input type="password" value={password} onChange={e => setPassword(e.target.value)} /> <button type="submit">Login</button> </form> ); // ユーザ情報 let userInfo = null; // 認証済みの場合、ログアウトボタンとユーザ情報を表示 if (user) { form = <button onClick={logout}>Logout</button>; userInfo = ( <div> <h2>User</h2> <div>name: {user.name}</div> <div>email: {user.email}</div> </div> ); } return ( <div> {form} {userInfo} <button onClick={getUser}>getUser</button> </div> ); }; export default Auth;
ビルドして確認。
ログインを試す
ログインのためのユーザーを作成します。 1つあればいいので、tinkerでサクッと作ります。
$ php artisan tinker >>> App\Models\User::factory()->create(['name' => 'hoge', 'email' => 'hoge@hoge.com', 'password' => Hash::make('password')]);
先ほど作成したフォームに
email : 'hoge@hoge.com
password : password
を打ち込み、ログインを試します。
ログインが成功し、画面にユーザー情報が表示されれば成功。 ログアウトせずにリロードをかけると勝手にログイン状態になるはず。
まとめ
パッケージがよしなにやってくれてラクチンですが
内部的に何が起きているのかはちゃんと理解して使いたいところ。