Laravel + Reactでログイン機能を作る方法

Laravel + Reactでログイン機能を作る方法

Laravel + Reactでログイン機能を作る場合、最初に決めるべきなのは「どの認証方式で進めるか」という点になる。見た目は同じログイン画面でも、実際の実装方法は大きく分けて2つある。1つは、LaravelのセッションとCookieを使ってReact側を認証する方法。もう1つは、APIトークンを発行してBearerトークンで認証する方法。LaravelとReactを同一サービスのSPAとして構築するなら、Laravel側のセッション認証とSanctumを組み合わせる構成が扱いやすい。この記事では、LaravelをAPIバックエンド、Reactをフロントエンドとした構成で、ログイン画面、CSRF対策、認証状態取得、ログアウト、エラー処理まで一通りつながる流れをまとめる。

最初に決めるべき認証方式

Laravel + Reactのログイン機能で最初に決めるべきなのは、次のどちらで進めるか。
・SPAとしてCookieベースで認証する
・APIトークンベースで認証する
同一サービスのReactフロントからLaravel APIへ接続するなら、Cookieベースの方が扱いやすいことが多い。
理由は、
・ログイン状態をLaravel側の標準的な仕組みで管理しやすい
・Bearerトークンを手動管理する必要がない
・CSRF保護やセッション認証と整合しやすい
から。
この記事では、Laravel + React のSPA構成として扱いやすい Cookie + Sanctum の流れを前提に進める。

全体の流れを最初に整理する

ログイン処理全体の流れは、次の順で考えると分かりやすい。

  1. ReactからCSRF Cookieを取得する
  2. ReactからログインAPIへメールアドレスとパスワードを送る
  3. Laravel側で認証成功ならセッションを開始する
  4. Reactから /api/user などへアクセスして認証済みユーザーを取得する
  5. 未認証ならログイン画面へ戻す
  6. ログアウト時はLaravel側でセッションを破棄する
    この流れを最初に把握しておくと、「どこで失敗しているか」が切り分けやすくなる。

Laravel側で用意するもの

Laravel側で最低限必要になるのは次の要素。
・ユーザーテーブル
・認証用ルート
・ログイン処理
・ログアウト処理
・認証済みユーザー取得API
・Sanctumの設定
既存のstarter kitを使うならかなりの部分が揃っているが、手動で組む場合でも最低限のルートとコントローラがあれば作れる。
React側から見れば、必要なのは「ログインできるAPI」「現在ログインしているユーザーを返すAPI」「ログアウトAPI」の3つが最低ラインになる。

React側で用意するもの

React側で最低限必要なのは次の通り。
・ログインフォーム
・入力状態(email / password)の管理
・ログイン中のローディング管理
・エラーメッセージ表示
・認証済みユーザー状態の管理
・未認証時の画面制御
ログイン機能では、単にフォームを作るだけでなく「今ログインしているのか」「ログイン中なのか」「失敗したのか」を状態として持つことが重要になる。

Laravelで認証用ルートを作る

まずはLaravel側でログイン、ログアウト、ユーザー取得のルートを用意する。
例としては次のような形になる。

use App\Http\Controllers\AuthController;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Route;

Route::post(‘/login’, [AuthController::class, ‘login’]);
Route::post(‘/logout’, [AuthController::class, ‘logout’]);
Route::get(‘/user’, [AuthController::class, ‘user’])->middleware(‘auth:sanctum’);

ここで重要なのは、/user のような認証済みAPIに auth:sanctum を付けること。
ログイン前でも叩けるルートと、ログイン後だけ使えるルートを分けておくと設計が整理しやすい。

Laravel側でログイン処理を実装する

ログイン処理では、送られてきたメールアドレスとパスワードを検証し、成功したらセッションを開始する。
シンプルな例は次のような形になる。

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\ValidationException;

class AuthController extends Controller
{
public function login(Request $request)
{
$credentials = $request->validate([
‘email’ => [‘required’, ‘email’],
‘password’ => [‘required’, ‘string’],
]);

if (! Auth::attempt($credentials, true)) {
throw ValidationException::withMessages([
‘email’ => [‘メールアドレスまたはパスワードが正しくありません。’],
]);
}

$request->session()->regenerate();

return response()->json([
‘message’ => ‘ログインしました’,
‘user’ => $request->user(),
]);
}

public function user(Request $request)
{
return response()->json($request->user());
}

public function logout(Request $request)
{
Auth::guard(‘web’)->logout();

$request->session()->invalidate();
$request->session()->regenerateToken();

return response()->json([
‘message’ => ‘ログアウトしました’,
]);
}
}

ここで起こりやすい問題は、
Auth::attempt() が常に false になる
・ハッシュ化されたパスワードを正しく照合できていない
・セッション再生成をしておらず、ログイン後の状態が不安定
といったもの。

Sanctumを前提にした現在ユーザー取得APIを用意する

React側では、ページ再読み込み後も「今ログイン中かどうか」を確認したい。
そのため、認証済みユーザーを返すAPIを1本持っておくと扱いやすい。

Route::get(‘/user’, function (Request $request) {
return response()->json($request->user());
})->middleware(‘auth:sanctum’);

React側はこのエンドポイントを叩いて、
・200ならログイン中
・401なら未ログイン
と判断しやすくなる。
アプリ全体の認証状態を決める基準点として使いやすい。

Reactのログインフォームを作る

フロント側では、まずログインフォームを作る。
最小構成の例は次の通り。

import { useState } from ‘react’;

export default function LoginPage() {
const [email, setEmail] = useState(”);
const [password, setPassword] = useState(”);
const [loading, setLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState(”);

const handleSubmit = async (e) => {
e.preventDefault();

try {
setLoading(true);
setErrorMessage(”);

// ここでCSRF Cookie取得とログインAPI送信を行う
} catch (error) {
setErrorMessage(‘ログインに失敗しました’);
} finally {
setLoading(false);
}
};

return (


setEmail(e.target.value)}
/>

setPassword(e.target.value)}
/>

{errorMessage &&

{errorMessage}

}


);
}

ここで重要なのは、入力値だけでなく、ローディング状態とエラー状態も持っていること。
ログイン処理は通信を伴うため、成功と失敗の両方を前提に設計した方が扱いやすい。

ReactからCSRF Cookieを取得する

Cookieベース認証では、ログインリクエストの前にCSRF Cookieを取得する必要がある。
これを飛ばすと、419系エラーやTokenMismatch系のエラーが起きやすい。
fetchで書くと次のような形になる。

await fetch(‘http://localhost/sanctum/csrf-cookie’, {
method: ‘GET’,
credentials: ‘include’,
});

その後にログインAPIを叩く。

const response = await fetch(‘http://localhost/login’, {
method: ‘POST’,
credentials: ‘include’,
headers: {
‘Content-Type’: ‘application/json’,
‘Accept’: ‘application/json’,
},
body: JSON.stringify({
email,
password,
}),
});

ここでのエラー発生条件は、
credentials: 'include' を付けていない
・CSRF Cookie取得を飛ばしている
・フロントとバックエンドのドメイン設定がずれている
というもの。

ログイン成功後に認証状態を取得する

ログインAPIが成功したら、そのままレスポンスで受け取ったユーザーを使ってもよいが、統一的には /api/user を再取得して「認証済み状態」を確定させる方が分かりやすい。
たとえば次のように組むことができる。

const login = async (email, password) => {
await fetch(‘http://localhost/sanctum/csrf-cookie’, {
credentials: ‘include’,
});

const loginResponse = await fetch(‘http://localhost/login’, {
method: ‘POST’,
credentials: ‘include’,
headers: {
‘Content-Type’: ‘application/json’,
‘Accept’: ‘application/json’,
},
body: JSON.stringify({ email, password }),
});

if (!loginResponse.ok) {
throw new Error(‘ログインに失敗しました’);
}

const userResponse = await fetch(‘http://localhost/api/user’, {
credentials: ‘include’,
headers: {
‘Accept’: ‘application/json’,
},
});

if (!userResponse.ok) {
throw new Error(‘ユーザー情報の取得に失敗しました’);
}

return await userResponse.json();
};

この形にしておくと、「ログインしたはずなのに状態が取れない」という不整合を見つけやすい。

認証状態をReact全体で管理する

ログイン機能では、ログインページだけでなく、アプリ全体で「今ログインしているか」を共有したくなる。
そのため、Contextや状態管理ライブラリを使って認証状態を全体に持つ構成が扱いやすい。
最小例としては次のようなContextを作る。

import { createContext, useContext, useEffect, useState } from ‘react’;

const AuthContext = createContext(null);

export function AuthProvider({ children }) {
const [user, setUser] = useState(null);
const [checking, setChecking] = useState(true);

useEffect(() => {
async function fetchUser() {
try {
const res = await fetch(‘http://localhost/api/user’, {
credentials: ‘include’,
headers: {
‘Accept’: ‘application/json’,
},
});

    if (res.ok) {
      const data = await res.json();
      setUser(data);
    } else {
      setUser(null);
    }
  } catch {
    setUser(null);
  } finally {
    setChecking(false);
  }
}

fetchUser();

}, []);

return (

{children}

);
}

export function useAuth() {
return useContext(AuthContext);
}

これで、画面ごとに「ログイン中か」「未認証か」を判定しやすくなる。

未認証ユーザーをログイン画面へ戻す

React Routerを使う場合は、未認証ならログイン画面へ戻すガードを作ると運用しやすい。
簡易的な例は次のようになる。

import { Navigate } from ‘react-router-dom’;
import { useAuth } from ‘./AuthContext’;

export default function ProtectedRoute({ children }) {
const { user, checking } = useAuth();

if (checking) {
return

認証状態を確認中…

;
}

if (!user) {
return ;
}

return children;
}

これを protected な画面に適用すれば、未ログインで直接URLアクセスされた場合も制御しやすい。

ログアウト処理を実装する

ログアウトではLaravel側でセッションを破棄し、React側でもユーザー状態を空にする。
たとえば次のような形になる。

const logout = async () => {
const response = await fetch(‘http://localhost/logout’, {
method: ‘POST’,
credentials: ‘include’,
headers: {
‘Accept’: ‘application/json’,
},
});

if (!response.ok) {
throw new Error(‘ログアウトに失敗しました’);
}

setUser(null);
};

ログアウト時によくある問題は、
・Laravel側ではログアウト済みなのにReact側でユーザー状態が残る
・逆にReact側だけ消してサーバーセッションが残る
という片肺状態。
フロントとバックエンドの両方で状態を合わせることが重要になる。

よくあるエラーと発生条件

Laravel + React のログイン機能でよく起きる問題は次の通り。
・419 / TokenMismatch
→ CSRF Cookie取得不足、credentials未設定
・401 Unauthorized
→ 未認証なのに保護APIを叩いている、Cookie未送信
・ログイン成功後に /api/user が取れない
→ セッション設定不整合、ドメイン設定ずれ
・ローカルでは動くのに本番でログインできない
SESSION_DOMAINSANCTUM_STATEFUL_DOMAINS、HTTPS設定不整合
・React側ではログインしたように見えるのに画面更新で消える
→ 認証状態を永続的に取り直していない
これらは単なるコードミスというより、Cookie、CSRF、セッション、状態管理の整合が崩れて起きることが多い。

入力エラーと認証エラーは分けて扱う

ログイン画面では、
・メールアドレス未入力
・パスワード未入力
・メール形式不正
・認証失敗
・通信失敗
を全部同じメッセージにすると使いづらい。
そのため、入力エラーはフォーム側、認証失敗はLaravelのレスポンス、通信失敗はフロントのcatch、と役割を分ける方が見通しが良い。
ログイン機能は単純に見えて、エラー種別を分けるだけでUXがかなり改善しやすい。

実務で扱いやすいまとめ方

Laravel + React のログイン機能を実務で組むときは、次の分け方がかなり扱いやすい。
・Laravel側: ログイン、ログアウト、現在ユーザー取得
・React側: 入力フォーム、認証状態管理、画面ガード
・通信レイヤ: CSRF Cookie取得、credentials付きfetch
・エラー表示: 入力ミス、認証失敗、通信失敗を分離
この構成にすると、「どこで壊れたか」が追いやすくなり、後から登録機能やパスワードリセットを追加する場合も拡張しやすい。

まとめ

Laravel + Reactでログイン機能を作る場合、最初に「Cookieベースで認証する」という前提を固めると、SanctumとLaravelの標準的な認証フローを活かしやすい。
実装の軸は、
・CSRF Cookie取得
・Laravel側ログイン処理
auth:sanctum 付きの現在ユーザー取得API
・React側の認証状態管理
・ログアウト処理
の5つになる。
単にログインフォームを作るだけではなく、「認証状態をどう維持するか」「エラーをどう分けるか」「未認証画面をどう制御するか」まで含めて設計すると、実務で壊れにくいログイン機能になりやすい。