【第四弾】Laravel入門資料

📚 Laravel公式ドキュメント - Authentication

https://laravel.com/docs/12.x/authentication

💡 この講座では、上記の公式ドキュメントを基に解説していきます。
公式ドキュメントは初学者には内容が難しいため、エッセンスを優しく噛み砕いて解説していきます。

スクラッチで認証機能を実装する

ライブラリに頼らず、自分の手で認証機能を作ります。
セッション、Cookie、ハッシュ化など、認証の仕組みを深く理解しましょう。

🎯 この章で学ぶこと

  • ✅ 認証の仕組み(セッション・Cookie)
  • ✅ ユーザー登録機能の実装
  • ✅ ログイン機能の実装
  • ✅ ログアウト機能の実装
  • ✅ ログイン状態の確認方法
  • ✅ セキュリティの考慮事項

認証の仕組み - セッションとは

Webアプリケーションの認証は、セッションを使って実現されます。

HTTPはステートレス

💡 ステートレスとは?

HTTPは「ステートレス」なプロトコルです。
つまり、リクエストごとに独立しており、前回のリクエストの情報を覚えていません。

  • • リクエスト1: ログインする → サーバー「OK、認証成功」
  • • リクエスト2: 商品一覧を見る → サーバー「あなた誰?」
  • • 問題: 毎回ログイン情報を送る必要がある

セッションで状態を保持する

✅ セッションの仕組み

1. ログイン成功
   ├─ サーバーがセッションIDを発行(例: abc123)
   ├─ セッションIDをCookieに保存
   └─ サーバー側でセッションデータを保存(ユーザーID、名前など)

2. 次回のリクエスト
   ├─ ブラウザが自動的にCookieを送信(abc123)
   ├─ サーバーがセッションIDからユーザー情報を取得
   └─ 「あなたは田中さんですね!」と識別できる

🔑 重要な用語

  • セッションID - ユーザーを識別する一意のID
  • Cookie - ブラウザに保存される小さなデータ(セッションIDを格納)
  • セッションストア - サーバー側でセッションデータを保存する場所(ファイル、Redis、DBなど)

ユーザー登録機能を作る

まずは、ユーザーがアカウントを作成できる機能を実装します。

ステップ1: usersテーブルのマイグレーション

Laravelの新規プロジェクトには、すでにusersテーブルのマイグレーションが含まれています。

# マイグレーション実行
php artisan migrate

生成されるテーブル構造:

// database/migrations/xxxx_create_users_table.php
public function up(): void
{
    Schema::create('users', function (Blueprint $table) {
        $table->id();
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });
}

💡 各カラムの意味

  • name - ユーザー名
  • email - メールアドレス(ログインID、ユニーク制約)
  • email_verified_at - メール認証日時(今回は使用しない)
  • password - ハッシュ化されたパスワード
  • rememberToken - Remember Me機能用(今回は使用しない)

ステップ2: Userモデルの確認

Userモデルもすでに用意されています。

// app/Models/User.php
namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

class User extends Authenticatable
{
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
        'password' => 'hashed', // Laravel 10以降、自動的にハッシュ化
    ];
}

💡 重要なポイント

  • Authenticatableを継承 - 認証機能を持つモデル
  • $fillable - 一括代入可能な属性(セキュリティ対策)
    User::create()で指定できるカラムを制限します

$hidden の役割 - JSONレスポンスでパスワードを隠す

$hidden は、モデルをJSON配列に変換する際に、特定の属性を隠すための設定です。

protected $hidden = [
    'password',
    'remember_token',
];

🔐 なぜ必要?

APIレスポンスやログ出力でユーザー情報をJSON化すると、パスワードのハッシュ値も含まれてしまいます
ハッシュ値でも流出すると攻撃のヒントになるため、$hiddenで除外します。

$hidden がない場合:

// コントローラーでユーザー情報をJSON化
$user = User::find(1);
return response()->json($user);

// ❌ レスポンス(パスワードのハッシュが含まれる)
{
    "id": 1,
    "name": "田中太郎",
    "email": "tanaka@example.com",
    "password": "$2y$10$abc123def456...",  // ← 流出してしまう!
    "created_at": "2025-01-01 00:00:00"
}

$hidden がある場合:

// 同じコード
$user = User::find(1);
return response()->json($user);

// ✅ レスポンス(パスワードが自動的に除外される)
{
    "id": 1,
    "name": "田中太郎",
    "email": "tanaka@example.com",
    "created_at": "2025-01-01 00:00:00"
}

💡 $hidden の使い所

  • • APIレスポンスでユーザー情報を返す時
  • • ログにユーザー情報を出力する時
  • toArray()toJson() を使う時

$casts の役割 - 自動型変換とハッシュ化

$casts は、データベースから取得した値や保存する値を自動的に変換する設定です。

⚠️ 重要: データベースは全て文字列で返す

MySQLやPostgreSQLからデータを取得すると、全て文字列として返されます。
そのため、castを使わないと毎回手動で型変換が必要になります。

// cast なし
$user->is_admin;           // "1" (文字列)
$user->price;              // "1000" (文字列)
$user->email_verified_at;  // "2024-01-01 12:34:56" (文字列)

// 毎回変換が必要
if ($user->is_admin === '1') { }  // ❌ めんどくさい
$price = (int)$user->price;       // ❌ めんどくさい

// cast あり
if ($user->is_admin) { }  // ✅ 自動でboolean
$price = $user->price;    // ✅ 自動でinteger
protected $casts = [
    'email_verified_at' => 'datetime',
    'password' => 'hashed',  // Laravel 10以降
];
1. 'datetime' キャストの動き
// データベースには文字列で保存されている
// email_verified_at: "2025-01-01 12:00:00"

$user = User::find(1);

// ✅ 自動的にCarbonオブジェクトに変換される
$user->email_verified_at->format('Y年m月d日');
// => "2025年01月01日"

$user->email_verified_at->addDays(7);
// => 7日後の日時を計算できる

💡 Carbon とは?

Carbon は PHP の日時操作ライブラリです(Laravel専用ではありません)。
'datetime' キャストを使うと、文字列が自動で Carbon オブジェクトに変換されます。

  • パッケージ名: nesbot/carbon
  • 開発元: Brian Nesbitt(Laravel公式ではない)
  • Laravel との関係: Laravelが内部で使っている外部ライブラリ
  • 利点: 日時の計算・フォーマットが簡単になる
// Carbon でできること
$date->format('Y年m月d日');    // フォーマット変換
$date->addDays(7);             // 日付計算
$date->diffForHumans();        // "3日前" のような相対表現
$date->isWeekend();            // 週末判定
2. 'hashed' キャストの動き(Laravel 10以降)

'password' => 'hashed' を設定すると、保存時に自動的にハッシュ化されます。

🎯 'hashed' キャストの仕組み

Laravelが内部で Hash::make() を自動実行してくれます。

Laravel 9以前('hashed' キャストがない場合):

// ❌ 手動でHash::make()を書く必要がある
$user = User::create([
    'name' => '田中太郎',
    'email' => 'tanaka@example.com',
    'password' => Hash::make('password123'),  // 手動でハッシュ化
]);

Laravel 10以降('hashed' キャストがある場合):

// ✅ 自動的にハッシュ化される
$user = User::create([
    'name' => '田中太郎',
    'email' => 'tanaka@example.com',
    'password' => 'password123',  // 平文のまま渡してOK
]);

// 保存時に自動的に Hash::make('password123') が実行される
// データベースには $2y$10$abc... のようなハッシュ値が保存される

⚠️ 注意点

  • • 'hashed' キャストは保存時のみ動作します
    取得時はハッシュ値のまま(元のパスワードには戻せない)
  • • 更新時も自動的にハッシュ化されます
    $user->update(['password' => 'newpassword']) も自動ハッシュ化
  • • すでにハッシュ化された値を再度保存すると二重ハッシュ化されるので注意
    パスワード更新時のみ代入するようにしましょう
3. 実践: 'hashed' キャストの動作確認
# Tinkerを起動
php artisan tinker
// ユーザーを作成(平文パスワードを渡す)
$user = User::create([
    'name' => 'テストユーザー',
    'email' => 'test@example.com',
    'password' => 'password123',  // 平文
]);

// データベースに保存されているパスワードを確認
// (通常は$hiddenで隠されるが、getAttributesで生の値を取得できる)
$user->getAttributes()['password'];
// => "$2y$10$abc123def456..."  ← 自動的にハッシュ化されている!

// ログインできるか確認
Hash::check('password123', $user->password);
// => true  ✅ ハッシュ化されたパスワードと照合できる

// パスワード更新も自動ハッシュ化
$user->update(['password' => 'newpassword']);
$user->getAttributes()['password'];
// => "$2y$10$xyz789..."  ← 新しいハッシュ値

✅ まとめ: $hidden と $casts の使い分け

設定 目的 使い所
$hidden JSON/配列変換時に属性を隠す APIレスポンス、ログ出力
$casts データ型を自動変換 日時操作、パスワードハッシュ化

ユーザー登録のコントローラーとルート・Facadeの解説

登録フォームを表示し、ユーザー情報を保存するコントローラーを作成します。

ステップ3: コントローラー作成

php artisan make:controller Auth/RegisterController
// app/Http/Controllers/Auth/RegisterController.php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

class RegisterController extends Controller
{
    // 登録フォーム表示
    public function showRegistrationForm()
    {
        return view('auth.register');
    }

    // ユーザー登録処理
    public function register(Request $request)
    {
        // バリデーション
        $validated = $request->validate([
            'name' => 'required|string|max:255',
            'email' => 'required|string|email|max:255|unique:users',
            'password' => 'required|string|min:8|confirmed',
        ]);

        // ユーザー作成
        $user = User::create([
            'name' => $validated['name'],
            'email' => $validated['email'],
            'password' => $validated['password'], // castで自動ハッシュ化
        ]);

        // 自動ログイン
        Auth::login($user);

        // ダッシュボードへリダイレクト
        return redirect('/dashboard');
    }
}

🔍 バリデーションルールの解説

  • unique:users
    → usersテーブルで重複チェック(メールアドレスが既に存在しないか)
    → DBにもUNIQUE制約があるが、バリデーションで先にチェックする理由:
    • • エラーメッセージがユーザーフレンドリー
    • • パスワードのハッシュ化(重い処理)を避けられる
    • • DBエラーを隠してセキュリティ向上
  • confirmed
    → パスワード確認フィールド(password_confirmation)と一致するかチェック
    → フィールド名は必ず {フィールド名}_confirmation にする(命名規則固定)
    → 例: 'password' => 'confirmed'password_confirmation フィールドを自動で探す

🔑 Auth::login($user) の仕組み

Auth::login($user) を実行すると、ユーザーを「ログイン状態」にします。

Auth::とは?(Facade の説明)

Auth::Facade(ファサード)と呼ばれる仕組みです。
複雑なクラスに対して、シンプルな静的メソッドでアクセスできるようにします。

📚 Facade とヘルパー関数の違い

Facade(ファサード)

  • • Laravel が用意した便利なクラス
  • use Illuminate\Support\Facades\XXX; でインポート
  • XXX::method() で静的メソッドのように使える

ヘルパー関数

  • auth(), session(), route() など
  • use 不要でどこでも使える
  • • PHP のグローバル関数
// Facade を使う(シンプル)
use Illuminate\Support\Facades\Auth;
Auth::login($user);

// Facade を使わない場合(複雑)
use Illuminate\Auth\AuthManager;
$auth = app(AuthManager::class);
$auth->guard()->login($user);

このセクションで出てくるFacade

use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;

// Auth Facade
Auth::login($user);
Auth::logout();
Auth::check();
Auth::user();

// Hash Facade
Hash::make('password');
Hash::check('password', $hash);

💡 Facadeは Illuminate\Support\Facades 名前空間にあります。
使う前に use でインポートが必要です。

auth() ヘルパー関数と Auth:: Facade の違い

// ヘルパー関数(use不要、どこでも使える)
auth()->login($user);
auth()->check();
auth()->user();

// Facade(useが必要、静的メソッド)
use Illuminate\Support\Facades\Auth;
Auth::login($user);
Auth::check();
Auth::user();

// どちらも内部的には同じ処理

どちらを使うべき?

  • Blade(ビュー): auth() ヘルパーを使う(useが不要なので楽)
  • コントローラー: どちらでもOK(チームの規約に従う)
  • Laravel公式: ヘルパー関数を推奨(短くてシンプル)
  • テスト: Facadeの方がモック化しやすい

💡 迷ったら auth() ヘルパーを使いましょう。

Tinkerで試してみよう

php artisan tinker

// Facade を使う
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;

// まずテストユーザーを作成
$user = User::create([
    'name' => 'テストユーザー',
    'email' => 'test@example.com',
    'password' => 'password123',  // castで自動ハッシュ化
]);

// Auth Facade
Auth::login($user);
Auth::check();  // => true
Auth::user()->name;  // => "テストユーザー"
Auth::logout();
Auth::check();  // => false

// Hash Facade
$hashed = Hash::make('password');
Hash::check('password', $hashed);  // => true

// DB Facade
DB::table('users')->count();  // => 1
DB::table('users')->where('id', 1)->first();

// Log Facade
Log::info('テスト実行');
Log::warning('警告メッセージ');
Log::error('エラー発生');
// storage/logs/laravel.log に出力される

⚠️ Tinker では auth() などのヘルパー関数が使えない場合があります。
その場合は Facade(Auth::)を使ってください。

セッションに保存される内容

// ログイン前のセッション
[
    "_token" => "abc123...",  // CSRFトークン
]

// Auth::login($user) 実行後
[
    "_token" => "abc123...",
    "login_web_xxxxx" => 1,   // ← ユーザーIDが追加される
]

重要ポイント

  • • セッションにはユーザーIDのみ保存される(名前やメールは保存されない)
  • auth()->user() を呼ぶと、セッションからIDを取得してDBから全情報を取得
  • • セキュリティ上、最小限の情報だけセッションに保存

ユーザー情報取得の流れ

// auth()->user() を呼ぶと
1. セッションからユーザーIDを取得(例: 1)
2. SELECT * FROM users WHERE id = 1 を実行
3. User オブジェクトを返す

// つまり、auth()->user() を呼ぶたびにDBクエリが発生
// (ただし1リクエスト内ではキャッシュされる)

よく使う Facade 一覧とメソッド例

// Auth - 認証
Auth::login($user);
Auth::logout();
Auth::check();
Auth::user();

// Hash - ハッシュ化
Hash::make('password');
Hash::check('password', $hash);

// DB - データベース操作
DB::table('users')->get();
DB::table('users')->where('id', 1)->first();
DB::insert('insert into users (name) values (?)', ['太郎']);

// Storage - ファイル操作
Storage::exists('file.txt');
Storage::delete('file.txt');
Storage::download('file.pdf');

// Log - ログ出力
Log::info('情報ログ');
Log::warning('警告ログ');
Log::error('エラーログ');

// Mail - メール送信
Mail::to('user@example.com')->send(new WelcomeMail());

// Session - セッション
Session::put('key', 'value');
Session::get('key');
Session::forget('key');
Session::flash('message', '保存しました');

// Cookie - クッキー
Cookie::queue('name', 'value', 60);
Cookie::get('name');

// Config - 設定値取得
Config::get('app.name');
Config::set('app.debug', false);

// View - ビュー
View::make('welcome');
View::exists('auth.login');

// Redirect - リダイレクト
Redirect::to('/home');
Redirect::route('dashboard');
Redirect::back();

// Validator - バリデーション
Validator::make($data, $rules);
Validator::make($data, ['email' => 'required|email']);

💡 実務では Auth, Hash, DB, Storage, Log をよく使います。
すべて覚える必要はありません。必要になったら公式ドキュメントで調べましょう。

ルートとviewの作成とヘルパー関数の解説

ステップ4: 登録フォームのビュー作成

{{-- resources/views/auth/register.blade.php --}}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ユーザー登録</title>
</head>
<body>
    <h1>ユーザー登録</h1>

    @if ($errors->any())
        <div style="color: red;">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <form method="POST" action="/register">
        @csrf

        <label>名前</label>
        <input type="text" name="name" value="{{ old('name') }}" required>

        <label>メールアドレス</label>
        <input type="email" name="email" value="{{ old('email') }}" required>

        <label>パスワード</label>
        <input type="password" name="password" required>

        <label>パスワード確認</label>
        <input type="password" name="password_confirmation" required>

        <button type="submit">登録</button>
    </form>

    <p>すでにアカウントをお持ちですか? <a href="/login">ログイン</a></p>
</body>
</html>

ステップ5: ダッシュボードのビュー作成

{{-- resources/views/dashboard.blade.php --}}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ダッシュボード</title>
</head>
<body>
    <h1>ダッシュボード</h1>

    @auth
        <p>ようこそ、{{ auth()->user()->name }}さん!</p>
        <p>メールアドレス: {{ auth()->user()->email }}</p>

        <form action="{{ route('logout') }}" method="POST" style="margin-top: 20px;">
            @csrf
            <button type="submit">ログアウト</button>
        </form>
    @endauth
</body>
</html>

💡 @auth ディレクティブ

@auth は「ログイン中のみ表示」するBladeディレクティブです。

@auth
    <p>ログイン中の表示</p>
@endauth

@guest
    <p>ログアウト中の表示</p>
@endguest

💡 auth()->user() の使い方

auth()->user() でログイン中のユーザー情報を取得できます。
コントローラーから渡さなくても、Bladeで直接使えます(グローバル関数のため)。

<p>{{ auth()->user()->name }}</p>
<p>{{ auth()->user()->email }}</p>

📚 ヘルパー関数とは?

ヘルパー関数は、Laravelが提供するグローバル関数です。
use不要でどこでも使えます。

実は、これまでのコードで既に何度も使っています!

これまで使ってきたヘルパー関数

auth()

// ログイン処理で使用
auth()->login($user);

// ダッシュボードで使用
auth()->user()->name

view()

// コントローラーで使用
return view('auth.register');

redirect()

// 登録後のリダイレクト
return redirect()->route('dashboard');

💡 redirect()route()で名前付きルートを使いましょう。
URLを直接書くと、ルート変更時に全て修正が必要になります。

old()

// バリデーションエラー時に前回の入力値を復元
<input type="email" name="email" value="{{ old('email') }}">

route()

// 名前付きルートのURL生成
<a href="{{ route('dashboard') }}">ダッシュボード</a>

// パラメータ付き
route('products.show', ['id' => 1])  // "/products/1"

dd()

// デバッグ用:変数の中身を見て処理を止める
dd($user);

実務でよく使うその他のヘルパー関数

session()

// セッションに保存
session(['cart' => $items]);

// セッションから取得
$cart = session('cart');

// フラッシュメッセージ(1回だけ表示)
session()->flash('success', '登録しました');

config()

// 設定値取得
config('app.name');      // "Laravel"
config('app.timezone');  // "Asia/Tokyo"

env()

// .envから環境変数取得
env('APP_ENV');      // "local"
env('DB_DATABASE'); // "laravel"

now() / today()

// 現在日時(Carbon)
now();                          // 2025-01-15 10:30:00
now()->format('Y年m月d日');      // "2025年01月15日"

// 今日の日付(Carbon)
today();                        // 2025-01-15 00:00:00

bcrypt()

// パスワードをハッシュ化(Hash::make()と同じ)
bcrypt('password123');

ステップ6: ルート設定(まとめて)

// routes/web.php
use App\Http\Controllers\Auth\RegisterController;

// 登録フォーム表示
Route::get('/register', [RegisterController::class, 'showRegistrationForm'])
    ->name('register');

// 登録処理
Route::post('/register', [RegisterController::class, 'register']);

// ダッシュボード(ログイン必須)
Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware('auth')->name('dashboard');

// ログアウト
Route::post('/logout', function (Request $request) {
    Auth::logout();
    $request->session()->invalidate();
    $request->session()->regenerateToken();
    return redirect('/');
})->name('logout');

🔐 ログアウトの3ステップ

1. Auth::logout()

セッションのpayloadからユーザーID(login_web_*)を削除します。

// DBの sessionsテーブルのpayloadカラム内のデータ

// ログアウト前のpayload
{
  "_token": "abc123...",
  "login_web_59ba36addc2b2f9401580f014c7f58ea4e30989d": 1,  ← これが消える
  "_previous": {...},
  "_flash": {...}
}

// Auth::logout() 実行後のpayload
{
  "_token": "abc123...",
  "_previous": {...},
  "_flash": {...}
}
// login_web_* が削除され、auth()->check() が false になる

💡 login_web_*のキー名は、ログインガードのハッシュ値です。
この値が消えると、Laravelは「ログインしていない」と判断します。

2. session()->invalidate()

セッションID自体を無効化して、新しいセッションIDを発行します。
以下の3つの処理を行います:

  • DBのsessionsテーブルから古いレコードを削除
  • 新しいセッションIDを生成
  • クッキーに新しいセッションIDをセット
// ① DBのsessionsテーブル
DELETE FROM sessions WHERE id = 'old_session_id_abc123';

// ② 新しいセッションID生成
新しいセッションID: xyz789

// ③ クッキーに新しいセッションIDをセット
Set-Cookie: laravel_session=xyz789; HttpOnly; SameSite=Lax

⚠️ なぜ必要?セッション固定攻撃を防ぐため

// 攻撃シナリオ(invalidate()しない場合)
1. 攻撃者が古いセッションID(abc123)を盗む
2. ユーザーがログアウト(Auth::logout()のみ)
   → DBのpayloadから login_web_* は消える
   → でもセッションID(abc123)は有効なまま
3. 攻撃者が盗んだセッションID(abc123)で再ログイン
4. セッションID(abc123)が再利用される → 危険

// invalidate()すると
1. ログアウト時にセッションID(abc123)を無効化
   → DBから削除され、新しいID(xyz789)を発行
2. 古いセッションID(abc123)は使えない → 安全

3. regenerateToken()

CSRFトークンを新しく生成します。
セッションのpayloadにある_tokenの値を変更します。

// DBの sessionsテーブルのpayloadカラム

// ログアウト前のpayload
{
  "_token": "abc123...",  ← これが変わる
  "_previous": {...}
}

// regenerateToken() 実行後のpayload
{
  "_token": "xyz789...",  ← 新しいトークン
  "_previous": {...}
}

// 古いトークン(abc123)でPOSTリクエストすると419エラーになる

⚠️ なぜ必要?古いフォームからの送信を無効化するため

// シナリオ
1. ユーザーがログアウト前にフォームを開いている
   → フォーム内の @csrf に古いトークン(abc123)が埋め込まれている
2. ログアウト後、そのフォームを送信
   → 古いトークン(abc123)で送信される
3. regenerateToken()していれば、419エラーで弾かれる → 安全

⚠️ 重要:3つの処理の関係

  • Auth::logout()セッションのpayloadから認証情報削除
  • invalidate()セッションID自体を無効化(DB削除 + Cookie更新)
  • regenerateToken()CSRFトークンを新規生成
  • 3つセットで実行しないと、セキュリティホールが残る

💡 Auth::logout()だけでもログアウトは機能しますが、
セキュリティを重視するなら3つセットが推奨されます(Laravel公式も推奨)。

実務では?
• 金融・医療系など高セキュリティ案件 → 必ず3つセット
• 一般的なWebアプリ → 3つセットが主流(念のため)
• 社内ツールや小規模案件 → Auth::logout()だけのことも

ログイン機能を作る

登録したユーザーでログインできるようにします。

ステップ1: LoginController作成

php artisan make:controller Auth/LoginController
// app/Http/Controllers/Auth/LoginController.php
namespace App\Http\Controllers\Auth;

use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    // ログインフォーム表示
    public function showLoginForm()
    {
        return view('auth.login');
    }

    // ログイン処理
    public function login(Request $request)
    {
        // バリデーション
        $credentials = $request->validate([
            'email' => 'required|email',
            'password' => 'required',
        ]);

        // 認証試行
        if (Auth::attempt($credentials)) {
            // セッション固定攻撃を防ぐ
            $request->session()->regenerate();

            // ログイン前にアクセスしようとしていたページへリダイレクト
            // なければダッシュボードへ
            return redirect()->intended(route('dashboard'));
        }

        // 認証失敗
        // onlyInput('email'): パスワードは復元しない(セキュリティのため)
        return back()->withErrors([
            'email' => 'メールアドレスまたはパスワードが正しくありません。',
        ])->onlyInput('email');
    }
}

💡 intended() とは?

redirect()->intended() は、ログイン前にアクセスしようとしていたページにリダイレクトします。

// シナリオ
1. 未ログインで /products/123 にアクセス
2. authミドルウェアが /login にリダイレクト
   (このとき、元のURL /products/123 をセッションに保存)
3. ログイン成功
4. intended() が保存されたURL /products/123 に戻す

// 元のURLがない場合(直接 /login にアクセスした場合)
→ 引数のデフォルトURL(route('dashboard'))にリダイレクト

📚 authミドルウェアの詳細は次の章で解説します
• 未ログイン時に自動的に /login にリダイレクトする仕組み
• リダイレクト先のカスタマイズ方法

💡 Auth::attempt() の仕組み

Auth::attempt(['email' => $email, 'password' => $password]) は以下を自動で行います:

  • 1. emailでユーザーをDBから検索
  • 2. Hash::check($password, $user->password) でパスワード照合
  • 3. 成功したらセッションにユーザーIDを保存
  • 4. trueまたはfalseを返す

🔐 session()->regenerate() の重要性

セッション固定攻撃を防ぐために、ログイン成功時に
セッションIDを再生成します。これは必須のセキュリティ対策です。

ステップ2: ルート設定

// routes/web.php
use App\Http\Controllers\Auth\LoginController;

// ログインフォーム表示
Route::get('/login', [LoginController::class, 'showLoginForm'])
    ->name('login');

// ログイン処理
Route::post('/login', [LoginController::class, 'login']);

ステップ3: ログインビュー作成

{{-- resources/views/auth/login.blade.php --}}
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ログイン</title>
</head>
<body>
    <h1>ログイン</h1>

    @if ($errors->any())
        <div style="color: red;">
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <form method="POST" action="/login">
        @csrf

        <label>メールアドレス</label>
        <input type="email" name="email" value="{{ old('email') }}" required>

        <label>パスワード</label>
        <input type="password" name="password" required>

        <button type="submit">ログイン</button>
    </form>

    <p>アカウントをお持ちでないですか? <a href="/register">新規登録</a></p>
</body>
</html>

✅ ログイン機能が完成しました!

ログイン状態の確認

認証状態を確認する方法

Laravelでは、ユーザーがログインしているかどうかを簡単に確認できます。

1. コントローラーやルートで確認

auth()->check() - ログインしているか確認

if (auth()->check()) {
    // ログイン中の処理
    echo 'ログインしています';
} else {
    // ログアウト中の処理
    echo 'ログインしていません';
}

auth()->user() - ログイン中のユーザー情報を取得

$user = auth()->user();

if ($user) {
    // ログイン中
    echo 'こんにちは、' . $user->name . 'さん!';
    echo 'あなたのメールアドレスは ' . $user->email . ' です。';
} else {
    // ログアウト中
    echo 'ログインしてください';
}

auth()->id() - ログイン中のユーザーIDを取得

$userId = auth()->id();

if ($userId) {
    echo 'あなたのユーザーIDは ' . $userId . ' です';
} else {
    echo 'ログインしていません';
}

auth() と Auth:: の違い:

  • auth()->check()Auth::check() は同じ
  • auth()->user()Auth::user() も同じ
  • auth() はヘルパー関数、Auth:: はファサード
  • どちらを使っても問題ありませんが、auth() の方が短くて読みやすいです

2. Bladeテンプレートで確認

Bladeテンプレートでは、@auth@guest ディレクティブを使って簡単に分岐できます。

<!-- ログイン中のみ表示 -->
@auth
    <p>こんにちは、{{ auth()->user()->name }}さん!</p>
    <form action="{{ route('logout') }}" method="POST">
        @csrf
        <button type="submit">ログアウト</button>
    </form>
@endauth

<!-- ログアウト中のみ表示 -->
@guest
    <p>ようこそ、ゲストさん!</p>
    <a href="{{ route('login') }}">ログイン</a>
    <a href="{{ route('register') }}">新規登録</a>
@endguest

@auth と @guest の使い分け:

<!-- どちらも表示する場合は @auth と @else -->
@auth
    <p>ログイン中: {{ auth()->user()->name }}</p>
@else
    <p>ログインしていません</p>
@endauth

<!-- ログイン中だけ表示する場合は @auth -->
@auth
    <p>ダッシュボードへようこそ!</p>
@endauth

<!-- ログアウト中だけ表示する場合は @guest -->
@guest
    <p>ログインしてください</p>
@endguest

3. ミドルウェアで認証を必須にする

ログインしていないとアクセスできないページを作る場合は、auth ミドルウェアを使います。

// routes/web.php

// ダッシュボード(ログイン必須)
Route::get('/dashboard', function () {
    return view('dashboard');
})->middleware('auth');

// プロフィール編集(ログイン必須)
Route::get('/profile/edit', [ProfileController::class, 'edit'])
    ->middleware('auth');

// 複数のルートにまとめて適用
Route::middleware(['auth'])->group(function () {
    Route::get('/dashboard', [DashboardController::class, 'index']);
    Route::get('/profile', [ProfileController::class, 'show']);
    Route::get('/settings', [SettingsController::class, 'index']);
});

authミドルウェアの動き:

  1. ユーザーが /dashboard にアクセス
  2. ミドルウェアがログイン状態を確認
  3. ログインしていれば → ダッシュボードを表示
  4. ログインしていなければ → /login にリダイレクト

📚 詳しくは次の章で!

ミドルウェアの仕組みや、カスタムミドルウェアの作成、ロール(権限)の実装については、
次の「ミドルウェア・ロール」の章で詳しく解説します。

  • ミドルウェアの内部の仕組み
  • カスタムミドルウェアの作成方法
  • リダイレクト先のカスタマイズ(/login 以外にする方法)
  • ロール(管理者・一般ユーザーなど)の実装

Tinkerで認証状態を確認してみよう

# Tinkerを起動
php artisan tinker
// ユーザーを取得
$user = User::first();

// ログインさせる
Auth::login($user);

// ログイン状態を確認
auth()->check();
// => true

auth()->user();
// => App\Models\User {id: 1, name: "田中太郎", ...}

auth()->id();
// => 1

// ログアウト
Auth::logout();

auth()->check();
// => false

auth()->user();
// => null

✅ これで、ログイン状態によって表示内容を切り替えることができます!

セキュリティ考慮事項

認証機能を実装する際には、セキュリティリスクに注意する必要があります。

1. パスワードのハッシュ化

❌ 絶対にやってはいけないこと:

// パスワードを平文で保存(絶対ダメ!)
User::create([
    'password' => $request->password, // ❌ 平文保存
]);

データベースが流出した場合、すべてのユーザーのパスワードが漏洩します。

✅ 正しい方法:

// パスワードをハッシュ化して保存
User::create([
    'password' => Hash::make($request->password), // ✅ ハッシュ化
]);

// Laravel 10以降なら自動でハッシュ化されるので、これでもOK
User::create([
    'password' => $request->password, // ✅ 自動ハッシュ化
]);

2. CSRF保護

CSRF(Cross-Site Request Forgery)とは?

悪意のあるサイトから、ユーザーの意図しないリクエストを送信させる攻撃です。

例: ユーザーがログイン中に悪意のあるサイトを開くと、勝手にパスワード変更のリクエストが送信される。

✅ 対策: @csrf ディレクティブを必ず含める

<form action="{{ route('login') }}" method="POST">
    @csrf  <!-- ✅ CSRFトークンを自動生成 -->
    <input type="email" name="email">
    <input type="password" name="password">
    <button type="submit">ログイン</button>
</form>

LaravelはCSRFトークンを自動的に検証し、不正なリクエストを拒否します。

3. セッション固定攻撃の防止

⚠️ セッション固定攻撃の具体例

攻撃者が事前にセッションIDを用意し、ユーザーにそのセッションIDを使わせることで、
ログイン後のセッションを乗っ取る攻撃です。

// 攻撃の流れ(regenerate()しない場合)

1. 攻撃者が自分のブラウザでアプリにアクセス
   → セッションID: abc123 が発行される

2. 攻撃者が被害者に罠リンクを送る
   https://example.com/login?session_id=abc123
   または
   <script>document.cookie="laravel_session=abc123"</script>

3. 被害者がリンクをクリック
   → セッションID: abc123 が被害者のブラウザにセットされる

4. 被害者がログイン成功
   → でもセッションID: abc123 のまま(regenerateしてないから)

5. 攻撃者が自分のブラウザでアクセス
   → セッションID: abc123 で被害者のアカウントにログイン状態
   → 被害者のアカウントを完全に乗っ取り成功 🔥

🔍 なぜこの攻撃が成立するのか?

  • ログイン前後で同じセッションIDを使い続けるから
  • 攻撃者が事前に知っているセッションID(abc123)が、ログイン後も有効なまま
  • 攻撃者と被害者が同じセッションIDを共有している状態になる

✅ 対策: ログイン時にセッションIDを再生成

// LoginController.php
if (Auth::attempt($credentials)) {
    // ✅ セッションIDを再生成して、古いセッションIDを無効化
    $request->session()->regenerate();
    return redirect('/dashboard');
}
// regenerate() すると

1. 攻撃者が被害者に罠リンクを送る
   → セッションID: abc123 がセットされる

2. 被害者がログイン成功
   → regenerate() が実行される
   → 古いセッションID(abc123)は削除される
   → 新しいセッションID(xyz789)が発行される

3. 攻撃者が自分のブラウザでアクセス
   → セッションID: abc123 で試みる
   → でもabc123は既に無効化されている
   → ログインできない ✅ 攻撃失敗

💡 regenerate() は、DBのsessionsテーブルから古いレコードを削除し、新しいIDを発行します。
これにより、攻撃者が事前に知っているセッションIDは使えなくなります。

🔐 実装のポイント

  • ログイン成功時に必ず regenerate() を実行
  • ログアウト時には invalidate() + regenerateToken() を実行
  • 権限が変わる操作(管理者昇格など)の後も regenerate() を実行

4. SQLインジェクション対策

❌ 絶対にやってはいけないこと:

// 生のSQLに直接ユーザー入力を埋め込む(絶対ダメ!)
$email = $request->email;
$user = DB::select("SELECT * FROM users WHERE email = '$email'"); // ❌

攻撃者が ' OR '1'='1 のような入力をすると、全ユーザー情報が漏洩します。

✅ 正しい方法: Eloquent ORM や Query Builder を使う

// ✅ Eloquent ORM(自動的にエスケープされる)
$user = User::where('email', $request->email)->first();

// ✅ Query Builderのプリペアドステートメント
$user = DB::table('users')
    ->where('email', $request->email)
    ->first();

5. XSS(クロスサイトスクリプティング)対策

XSSとは?

悪意のあるスクリプトをWebページに埋め込んで実行させる攻撃です。

✅ 対策: Bladeの {{ }} を使う(自動エスケープ)

<!-- ✅ 自動的にHTMLエスケープされる -->
<p>ようこそ、{{ $user->name }}さん!</p>

<!-- ❌ エスケープされない(危険!) -->
<p>ようこそ、{!! $user->name !!}さん!</p>

{{ }} を使うと、<script>alert('XSS')</script> のような入力も安全に表示されます。

6. パスワードリセット時の注意点

注意すべきこと:

  • リセットトークンは1回限り有効にする
  • リセットトークンには有効期限を設ける(通常60分)
  • パスワードリセットのメールは誰にでも送信できるようにする(メールアドレスの存在を推測されないため)

7. レート制限(ログイン試行回数制限)

ブルートフォース攻撃対策:

短時間に何度もログインを試行する攻撃を防ぐため、ログイン試行回数を制限します。

// LoginController.php
use Illuminate\Support\Facades\RateLimiter;
use Illuminate\Validation\ValidationException;

public function login(Request $request)
{
    // レート制限キー(メールアドレス+IPアドレス)
    $key = 'login:' . $request->email . ':' . $request->ip();

    // 5回失敗したら1分間ロック
    if (RateLimiter::tooManyAttempts($key, 5)) {
        $seconds = RateLimiter::availableIn($key);
        throw ValidationException::withMessages([
            'email' => "ログイン試行回数が多すぎます。{$seconds}秒後に再試行してください。",
        ]);
    }

    $credentials = $request->validate([
        'email' => 'required|email',
        'password' => 'required',
    ]);

    if (Auth::attempt($credentials)) {
        // 成功したらカウンターをクリア
        RateLimiter::clear($key);
        $request->session()->regenerate();
        return redirect('/dashboard');
    }

    // 失敗したらカウンターを増やす
    RateLimiter::hit($key, 60); // 60秒間保持

    // onlyInput('email'): パスワードは復元しない(セキュリティのため)
    return back()->withErrors([
        'email' => 'メールアドレスまたはパスワードが正しくありません。',
    ])->onlyInput('email');
}

✅ セキュリティ対策を理解して、安全な認証機能を実装しましょう!

まとめ

この章で学んだこと

  • セッション - HTTPはステートレス、セッションでログイン状態を保持
  • Userモデル - $fillable、$hidden、$casts(パスワード自動ハッシュ化)
  • Facade - Auth::、Hash::、DB:: などの便利なクラス
  • ヘルパー関数 - auth()、view()、redirect()、route()、old()、dd()
  • ユーザー登録 - バリデーション、ハッシュ化、自動ログイン
  • ログイン - Auth::attempt()、intended()、セッション再生成
  • ログアウト - Auth::logout()、invalidate()、regenerateToken() の3ステップ
  • 認証状態確認 - auth()->check()、auth()->user()、@auth / @guest
  • セキュリティ - CSRF保護、セッション固定攻撃、SQLインジェクション、XSS

次のステップ

この章では、認証機能の基本を学びました。
次の章では、ミドルウェアとロール(権限管理)を学びます。

  • 認証と認可の違い - 「ログイン済み」と「権限あり」の違い
  • ミドルウェアの仕組み - リクエストを処理する前にチェック
  • ロールテーブルの設計 - 管理者・編集者・閲覧者などの権限管理
  • カスタムミドルウェアの作成 - 自分で認可ロジックを実装
  • Gate と Policy - より柔軟な権限管理
  • Tinkerで実践 - ロール割り当てと権限チェック

🎉 認証機能実装の章が完了しました!

Breezeなどのパッケージを使わずに、認証の仕組みを理解しながら実装できました。
次は「ミドルウェアとロール」の章で、より実践的な権限管理を学びましょう!