2026年5月25日

2026年5月25日

Laravelの認証(Auth)機能の実装方法|スターターキットと手書きの両方を解説

Laravel でログイン機能を作るなら、まずは公式のスターターキットを使い、仕組みを理解したい部分や独自の要件がある部分だけを Auth ファサードで手書きするのが安全です。この記事では、認証パッケージの選び方、スターターキットでの導入手順、Auth::attempt() を使った登録・ログイン・ログアウトの実装、そしてレート制限やセッション固定対策など、本番で欠かせないセキュリティ上の要点までを説明します。

認証の選択肢を整理する

Laravel の認証まわりには複数のパッケージがあり、役割が少しずつ違います。名前と提供状況はバージョンで変わるため、導入前に公式ドキュメントの「Starter Kits」と「Authentication」のページを確認してください(2026年10月現在)。

  • スターターキット(React / Vue / Livewire): Laravel 12 から提供されている雛形で、ログイン、ユーザー登録、パスワードリセット、メール認証、プロフィール画面などが最初から入ります。画面とコードがプロジェクト内にコピーされるので、自由に書き換えられます。
  • Laravel Breeze / Jetstream: 以前のスターターキットです。既存プロジェクトで使われていることは多いものの、新規には上記のスターターキットが案内されています。
  • Laravel Fortify: 画面を持たない認証バックエンドです。ログイン、登録、パスワードリセット、二要素認証などのルートと処理だけを提供し、画面は自分で作ります。
  • Laravel Sanctum: SPA やモバイルアプリ向けに、Cookie ベースのセッション認証や API トークンを扱います。
  • Laravel Passport: 自分のアプリを OAuth2 サーバーにしたいときに使います。単に API を守りたいだけなら、まず Sanctum で足りるかを検討します。

「自前で全部書く」のは学習には役立ちますが、パスワードリセットのトークン管理やメール認証の署名付き URL まで手書きすると、見落としが生まれやすくなります。本番ではスターターキットか Fortify を土台にするのがおすすめです。

スターターキットで導入する手順

新規プロジェクトなら、Laravel インストーラーで作成時にスターターキットを選ぶのがいちばん簡単です。

laravel new my-app
cd my-app
npm install && npm run build
php artisan migrate
composer run dev

インストーラーの質問に沿って React・Vue・Livewire のいずれかを選びます。作成後、/register でユーザーを登録し、/dashboard にログイン後だけ入れることを確認してください。選択肢や生成されるファイルはバージョンによって異なるため、画面の表示と生成されたルートファイル(routes/ 配下)を見て、どの URL がどのコントローラーにつながっているかを把握しておくと、後のカスタマイズが楽になります。

作業は Git でコミットしてから行い、既存プロジェクトに導入する場合はステージング環境で試してから本番に反映します。

Auth ファサードで手書きする

ここからは、仕組みを理解するために最小限のログイン機能を自分で書きます。スターターキットの中でも、同じ考え方の処理が使われています。

ユーザーモデルとパスワードのハッシュ化

Laravel の雛形にある App\Models\User では、password 属性に hashed キャストが設定されています。これにより、平文を代入しても保存時に自動でハッシュ化されます。パスワードを自前で暗号化したり、平文のままデータベースやログに残したりしないことが大前提です。

ユーザー登録

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rules\Password;

class RegisterController extends Controller
{
    public function create()
    {
        return view('auth.register');
    }

    public function store(Request $request)
    {
        $validated = $request->validate([
            'name' => ['required', 'string', 'max:255'],
            'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:users,email'],
            'password' => ['required', 'confirmed', Password::defaults()],
        ]);

        // password は User モデルの hashed キャストで保存時にハッシュ化される
        $user = User::create($validated);

        event(new Registered($user));

        Auth::login($user);
        $request->session()->regenerate();

        return redirect('/dashboard');
    }
}

confirmed ルールは、フォームに password_confirmation という入力欄があることを前提にしています。Password::defaults() の中身は AppServiceProvider の boot() で Password::defaults(fn () => Password::min(12)->mixedCase()); のように定義でき、全画面で同じ基準を使えます。uncompromised() を加えると漏えい済みパスワードの照合も行えますが、外部サービスへの問い合わせが発生する点を理解したうえで使ってください。

ログインとログアウト(レート制限つき)

<?php

namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;

class LoginController extends Controller
{
    public function create()
    {
        return view('auth.login');
    }

    public function store(Request $request)
    {
        $credentials = $request->validate([
            'email' => ['required', 'string', 'email'],
            'password' => ['required', 'string'],
        ]);

        // メールアドレスと IP の組み合わせで試行回数を数える
        $key = Str::transliterate(Str::lower($credentials['email']).'|'.$request->ip());

        if (RateLimiter::tooManyAttempts($key, 5)) {
            $seconds = RateLimiter::availableIn($key);

            throw ValidationException::withMessages([
                'email' => "試行回数が上限に達しました。{$seconds}秒後にお試しください。",
            ]);
        }

        if (! Auth::attempt($credentials, $request->boolean('remember'))) {
            RateLimiter::hit($key, 60); // 失敗を記録し、60秒で減衰

            throw ValidationException::withMessages([
                'email' => 'メールアドレスまたはパスワードが正しくありません。',
            ]);
        }

        RateLimiter::clear($key);
        $request->session()->regenerate();

        return redirect()->intended('/dashboard');
    }

    public function destroy(Request $request)
    {
        Auth::logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return redirect('/');
    }
}

ポイントは3つあります。1つ目は、ログイン成功直後の session()->regenerate() です。セッション ID を作り直すことで、ログイン前の ID を悪用するセッション固定攻撃を防ぎます。2つ目は、エラーメッセージで「メールアドレスが存在しない」と「パスワードが違う」を区別しないことです。区別すると、登録済みアドレスの探索に使われます。3つ目はレート制限で、総当たり攻撃の速度を落とします。

ルートと画面

use App\Http\Controllers\Auth\LoginController;
use App\Http\Controllers\Auth\RegisterController;

Route::middleware('guest')->group(function () {
    Route::get('/register', [RegisterController::class, 'create'])->name('register');
    Route::post('/register', [RegisterController::class, 'store']);
    Route::get('/login', [LoginController::class, 'create'])->name('login');
    Route::post('/login', [LoginController::class, 'store']);
});

Route::middleware('auth')->group(function () {
    Route::get('/dashboard', fn () => view('dashboard'))->name('dashboard');
    Route::post('/logout', [LoginController::class, 'destroy'])->name('logout');
});

auth ミドルウェアは、未ログインのユーザーを login という名前のルートへリダイレクトします。ログイン画面のルートに ->name('login') を付け忘れると、エラーになるので注意してください。ログアウトを GET ではなく POST にしているのは、外部サイトのリンクや画像タグで勝手にログアウトさせられないようにするためです。

ログインフォームには @csrf を入れます。

<form method="POST" action="{{ route('login') }}">
    @csrf
    <input type="email" name="email" value="{{ old('email') }}" required autocomplete="username">
    @error('email') <p>{{ $message }}</p> @enderror
    <input type="password" name="password" required autocomplete="current-password">
    <label><input type="checkbox" name="remember"> ログイン状態を保持</label>
    <button type="submit">ログイン</button>
</form>

old('email') でメールアドレスだけを再表示し、パスワードは再表示しません。{{ }} による出力は自動でエスケープされます。

認証のあとに考えること

ログインしているかどうか(認証)と、そのユーザーに操作が許されているか(認可)は別の問題です。「ログイン済みなら誰でも他人の投稿を編集できる」状態にならないよう、Gate や Policy を定義し、コントローラーで Gate::authorize('update', $post) のように権限を確認します(Laravel 11 以降の雛形では、基底の Controller クラスに authorize() メソッドを持つトレイトが含まれていないため、ここでは Gate ファサードを使う書き方にしています)。

メールアドレスの確認が必要なら、User モデルに MustVerifyEmail インターフェースを実装し、保護したいルートに verified ミドルウェアを付けます。二要素認証やパスワードリセットは、Fortify やスターターキットの実装を使うほうが安全です。

本番運用のセキュリティチェック

  • HTTPS とセッション Cookie: 本番は HTTPS で配信し、.env の SESSION_SECURE_COOKIE=true で Cookie を HTTPS 限定にします。
  • ログに平文を残さない: Log::info($request->all()) のようにリクエスト全体を記録すると、パスワードがログに残ります。記録する項目は明示的に選んでください。
  • ワンタイムコードの扱い: 独自に確認コードを送る場合も、コードは平文で保存せずハッシュ化し、有効期限と試行回数の上限を設けます。
  • 管理者が締め出されたときの復旧手段: レート制限の記録はキャッシュに保存されるため、サーバーにログインできれば php artisan cache:clear で解除できます。パスワードを忘れた場合に備え、パスワードリセットのメールが届くことを事前に確認しておきます。php artisan tinker で直接変更する方法もありますが、シェルの履歴にパスワードが残らないよう扱いに気をつけてください。
  • 個人情報の扱い: メールアドレスや氏名は個人情報です。保存する項目を必要最小限にし、利用目的をプライバシーポリシーに記載します。

まとめ

  • 本番の認証はスターターキットか Fortify を土台にし、API は Sanctum、OAuth2 サーバーが必要なら Passport を検討する
  • パスワードは hashed キャストに任せ、平文で保存・記録しない
  • ログイン成功時は session()->regenerate()、ログアウト時は invalidate() と regenerateToken() を呼ぶ
  • RateLimiter で試行回数を制限し、エラーメッセージでアカウントの有無を漏らさない
  • 認証とは別に、Gate や Policy で認可を確認する

お気軽にご相談ください

お見積りへ お問い合わせへ
▲