こんにちは、新卒エンジニアの鈴木です!
このブログでは、Laravelを学んでいく過程を発信しています。前回は、Laravelで「ひとこと掲示板」を作るための土台としてMVCモデルの考え方やデータベースの準備までを行いました。 【Laravel入門】MVCモデルを実践して掲示板アプリを作ってみよう
今回は、ログインやログアウト、アカウントの新規作成など、認証まわりを実装していきます。
ユーザーから入力された情報を安全に扱うための「バリデーション」やログイン情報の管理方法についても、Laravelの便利な機能を使いながら学んでいきましょう。
目次
なぜ認証機能が重要なのか?
Webにおける「状態」の管理
Webアプリにおいて「誰が」操作しているかを特定し、なりすましやデータの不正な改ざんを防ぐためには認証機能が不可欠です。
しかし、Webの通信は基本的に「状態を持たない(ステートレス)」ため、サーバーはアクセスのたびに「このリクエストが誰からのものか」を忘れてしまいます。そこで、ログイン状態を保持する仕組みを導入し、サーバーにユーザーを継続的に識別させる必要があります。
これにより、投稿データとユーザーを紐付けたり本人のみが編集可能といった権限管理ができるようになり、初めて安全なアプリケーションとして機能するのです。
アカウントの新規登録機能
ここからは、アカウントを新規作成する機能を実装していきます。安全なアプリケーションを作るために、以下の順序で開発を進めます。
-
- バリデーション(入力データのチェック)
- コントローラー(登録処理の実装)
- ルーティング(URLの設定)
- ビュー(入力画面の作成)
1. バリデーション
バリデーションとは、直訳すると「検証」や「妥当性の確認」という意味です。ユーザーが入力したデータや、システムが処理しようとしているデータが、事前に決められたルールや形式に合っているかをチェックする作業のことを指します。
もし、このチェック(バリデーション)がなければ、システムは不正なデータや間違ったデータを受け入れてしまい、システムのエラーやクラッシュ、さらにはセキュリティリスクのような問題が発生する可能性があります。
バリデーションは、これらのトラブルを未然に防ぎ、データの品質とシステムの安定性を保つために欠かせません。
それでは、実際にユーザーが入力したデータが正しい形式かどうかを検証するクラスを作成していきましょう。以下のコマンドでRegisterRequestクラスを作成し、バリデーションルールを定義します。
php artisan make:request Auth/RegisterRequest
//app/Http/Requests/Auth/RegisterRequest
namespace App\Http\Requests\Auth;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rules\Password;
class RegisterRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'confirmed', Password::defaults()],
];
}
}
authorize()メソッドは、このリクエストを実行する権限があるかどうかをチェックするメソッドです。今回は「アカウント新規作成」であり、まだログインしていないユーザーが操作するため、無条件で許可としています。
rules()では、ユーザーが入力したデータに対してバリデーションルールを定めています。
たとえばnameに対しては、required(必須項目)、string(文字列型)、max:255(最大255文字まで)のようなルールを定めています。このように複数のルールを配列で渡すことで、厳密なチェックが可能になります。
少し特殊なのが、Password::defaults()です。これはアプリケーション全体で共通のパスワードルールを適用するための便利な機能で、個別に書く必要がなくなるため、保守性が向上します。現時点では共通ルールの中身が決まっていないため、設定を行いましょう。app\Providers\AppServiceProviderに以下のコードを記述してください。
//app/Providers/AppServiceProvider
use Illuminate\Validation\Rules\Password;
public function boot(): void
{
Password::defaults(function () {
return Password::min(8) // 最低8文字
->letters() // 文字を含む
->mixedCase() // 大文字と小文字を含む
->numbers() // 数字を含む
->symbols() // 記号を含む
->uncompromised(); // 漏洩したパスワードでないかチェック
});
}
先ほどRegisterRequest内でPassword::defaults()と記述した箇所に、ここで設定した強力なパスワードルールが一括で適用されるようになります。
これでバリデーションの準備は整いました。次は、実際にデータを受け取って登録処理を行うコントローラーを作成していきましょう。
2. ユーザー情報の登録処理(Controller)
コントローラーでは、アカウントを新規作成する処理を記述するだけでなく、画面の表示も制御します。まずは画面表示の機能から作成しましょう。
php artisan make:controller Auth/RegisterController
いつものコマンドでコントローラーを作成し、以下の処理を記述してください。
//App/Http/Controllers/Auth/RegisterController
class RegisterController extends Controller
{
/**
* 新規アカウント登録画面表示
*/
public function showRegistrationForm()
{
return view('auth.register');
}
}
view()を使うことで、引数に指定したBladeテンプレートを表示することができます。これは前回の記事でもやりましたね。
それでは、先ほど作成したRegisterRequestを受け取り、バリデーションを通過した安全なデータだけを使ってアカウントの新規登録を実装します。useを忘れないようにしてください。
//App/Http/Controllers/Auth/RegisterController
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use App\Models\User;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\Auth\RegisterRequest;
use \Hash;
class RegisterController extends Controller
{
//省略
/**
* 新規アカウント登録処理
*/
public function register(RegisterRequest $request)
{
// 1. バリデーション済みのデータを安全に取得
$validated = $request->validated();
// 2. パスワードをハッシュ化
$validated['password'] = Hash::make($validated['password']);
// 3. ユーザーを作成
$user = User::create($validated);
// 4. ユーザーをログインさせる
Auth::login($user);
// セッション再生成
$request->session()->regenerate();
return redirect('/posts')
->with('success', 'アカウント登録が完了しました');
}
}
上記のコードでは次の処理を行っています。
-
バリデーション済みのデータを取得
$request->validated()を使うことで、作成したRegisterRequestのルールを通過した安全なデータだけを配列として取得します。これにより、予期せぬデータの混入を防ぎます。 -
パスワードのハッシュ化
ここで最も重要なのが 「パスワードをそのまま保存しない」ということです。もしパスワードを平文でデータベースに保存してしまうと、万が一データベースの中身が流出した際に、ユーザーのパスワードが第三者に丸見えになってしまいます。これはセキュリティ上、絶対に避けるべき欠陥です。
そのため、Laravelが提供するHash::make()を使用して、パスワードを暗号化(ハッシュ化)します。ハッシュ化された文字列は元のパスワードに戻すことが極めて困難なため、セキュリティを強固に保つことができます。 -
ユーザーをデータベースに保存
User::create($validated)を実行することで、App\Models\Userモデルを経由してデータベースに新しいレコードを作成します。バリデーション済みのデータにハッシュ化したパスワードを上書きしているので、安全な状態でユーザー情報が登録されます。 -
自動ログインとセッションの再生成
登録処理が終わった後、わざわざログイン画面に戻って入力させるのはユーザーにとって手間です。そこでAuth::login($user)を使い、登録と同時にログイン状態にします。また、セキュリティ対策(セッション固定攻撃の防止)として、ログインのタイミングで必ず$request->session()->regenerate()を呼び出し、セッションIDを新しく発行し直しています。セッションについては次の章で詳しく解説します。 -
リダイレクトと完了メッセージ
処理完了後、redirect()で記事一覧画面へ遷移させます。同時にwith()で成功メッセージを渡し、画面上でユーザーに登録完了を伝えます。
3. ルーティング
画面表示用のメソッドとアカウント新規登録処理のメソッドが完成したので、前回の記事同様、ルーティングを行います。
//routes/web.php
use App\Http\Controllers\Auth\RegisterController; // 忘れずにインポート!
// 新規アカウント登録
Route::get('/register', [RegisterController::class, 'showRegistrationForm'])->name('register');
Route::post('/register', [RegisterController::class, 'register']);
1行目:画面表示用(GET)ブラウザで/registerにアクセスしたときに実行されます。
name('register')で名前を付けておくと、Bladeファイル(View)から route('register')という形で簡単にリンクを生成できて便利です。これは、今後ログイン画面からアカウント新規登録画面に遷移するときに役立ちます。
2行目:登録処理用(POST)フォームの送信先です。同じURL(/register)ですが、フォーム送信はPOSTメソッドで行われるため、Laravelが自動的にregisterメソッドへ処理を振り分けてくれます。
4. 登録フォームの作成(View)
最後に、ユーザーがアカウント情報を入力するための画面を作成します。Bladeテンプレートを使い、名前、メールアドレス、パスワード、パスワード(確認用)の各入力フィールドを設置します。コントローラーに記述したview('auth.register')とパスの齟齬がないよう気を付けてください。
//resources/views/auth/register.blade.php
@extends('layouts.auth')
@section('title', '新規登録')
@section('content')
<form method="POST" action="{{ route('register') }}">
@csrf
<div class="form-group">
<label for="name">名前 <span class="required">*</span></label>
<input type="text"
name="name"
id="name"
value="{{ old('name') }}"
class="form-control @error('name') is-invalid @enderror"
autofocus>
@error('name')
<div class="error-message">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="email">メールアドレス <span class="required">*</span></label>
<input type="email"
name="email"
id="email"
value="{{ old('email') }}"
class="form-control @error('email') is-invalid @enderror">
@error('email')
<div class="error-message">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="password">パスワード <span class="required">*</span></label>
<input type="password"
name="password"
id="password"
class="form-control @error('password') is-invalid @enderror">
<small class="input-hint">
8文字以上、大文字・小文字・数字・記号を含む
</small>
@error('password')
<div class="error-message">{{ $message }}</div>
@enderror
</div>
<div class="form-group">
<label for="password_confirmation">パスワード(確認) <span class="required">*</span></label>
<input type="password"
name="password_confirmation"
id="password_confirmation"
class="form-control">
</div>
<button type="submit" class="btn-submit">登録</button>
<div class="auth-footer">
<p>既にアカウントをお持ちの方</p>
<a href="#">ログインはこちら</a>
</div>
</form>
@endsection
ここで重要な点は大きく3つです。
-
@extendsと@sectionについて。これらを記述することで、共通のレイアウト(枠組み)を継承し、そのなかに今回の登録フォームを埋め込むことができます。formタグのactionについて。ここに{{ route('register') }}を記述することで、submit時に新規アカウント登録機能を発火させることができます。@errorで囲まれている要素についてです。@errorはバリデーションエラーがあったときのみ表示されます。
現状のまま画面を表示しようとするとエラーになってしまいます。 冒頭で@extends('layouts.auth')と記述しているにもかかわらず、その継承元となるレイアウトファイルがまだ存在しないためです。
レイアウト継承を利用すると、ヘッダーやフッターなどの共通部分を一箇所で管理できたり、「ゲストユーザー専用のデザイン」といった出し分けもスムーズに実装できるようになります。
それでは、画面が正しく動くようにレイアウトファイルを作成しましょう。
//resources/views/layout/auth.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title') - {{ config('app.name') }}</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body { background-color: #f5f5f5; font-family: sans-serif; display: flex; justify-content: center; align-items: center; min-height: 100vh; }
.auth-box { background: white; padding: 2rem; border-radius: 8px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); width: 100%; max-width: 400px; }
.auth-title { text-align: center; margin-bottom: 1.5rem; color: #333; }
.alert-error { background-color: #fee2e2; color: #991b1b; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; border: 1px solid #fca5a5; }
.form-group { margin-bottom: 1.25rem; }
.form-group label { display: block; margin-bottom: 0.5rem; font-weight: bold; color: #4b5563; }
.form-control { width: 100%; padding: 0.75rem; border: 1px solid #d1d5db; border-radius: 4px; font-size: 1rem; }
.form-control:focus { outline: none; border-color: #2563eb; box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2); } .
form-control.is-invalid { border-color: #dc2626; background-color: #fef2f2; }
.error-message { color: #dc2626; font-size: 0.875rem; margin-top: 0.25rem; }
.input-hint { display: block; color: #6b7280; font-size: 0.8rem; margin-top: 0.25rem; }
.btn-submit { width: 100%; background-color: #2563eb; color: white; padding: 0.75rem; border: none; border-radius: 4px; font-size: 1rem; font-weight: bold; cursor: pointer; transition: background-color 0.2s; } .btn-submit:hover { background-color: #1d4ed8; }
.auth-footer { text-align: center; margin-top: 1.5rem; font-size: 0.9rem; }
.auth-footer a { color: #2563eb; text-decoration: none; } .auth-footer a:hover { text-decoration: underline; }
.required { color: #dc2626; margin-left: 2px; }
</style>
</head>
<body>
<div class="auth-box">
<h1 class="auth-title">@yield('title')</h1>
@if(session('error'))
<div class="alert-error">
{{ session('error') }}
</div>
@endif
@yield('content')
</div>
</body>
</html>
これでアカウント新規登録機能は完成しました。実際に動かしてみましょう。

登録処理はしっかり動いているようですが、エラーが英語で表示されていますね。これは日本語に直しておきましょう。以下のファイルを作成してください。
//lang/ja/validation.php
return [
/**
* 一般的なバリデーションメッセージ
*/
'required' => ':attributeは必須項目です。',
'email' => ':attributeは正しいメールアドレス形式で入力してください。',
'string' => ':attributeは文字列を入力してください。',
'max' => [
'string' => ':attributeは:max文字以内で入力してください。',
],
'confirmed' => ':attributeが確認用入力と一致しません。',
'unique' => 'この:attributeは既に使用されています。',
/**
* Password::defaults() に関連するルール
*/
'min' => [
'string' => ':attributeは:min文字以上で入力してください。',
],
'password' => [
'letters' => ':attributeには文字を1つ以上含めてください。',
'mixed' => ':attributeには大文字と小文字を1つ以上含めてください。',
'numbers' => ':attributeには数字を1つ以上含めてください。',
'symbols' => ':attributeには記号を1つ以上含めてください。',
'uncompromised' => ':attributeは過去に漏洩したことがあります。別のパスワードを使用してください。',
],
/*
* 項目名の日本語化
*/
'attributes' => [
'name' => '名前',
'email' => 'メールアドレス',
'password' => 'パスワード',
'password_confirmation' => '確認用パスワード',
],
];
このファイルは、Laravelが出力するエラーメッセージを日本語に翻訳するための、辞書のような役割を果たしています。
コードのなかにある:attributeや:minといった記述は「プレースホルダー」と呼ばれ、画面表示の際に自動的に具体的な項目名や数字に置き換わります。たとえば、パスワードの文字数不足であればLaravelが数値を埋め込んで「パスワードは8文字以上……」というメッセージを生成してくれます。
特に重要なのが、attributesという項目です。ここで「email は『メールアドレス』」のように定義しておくことで、エラーメッセージの主語が「emailは必須項目です」といったアルファベットのままで表示されることなく、「メールアドレスは必須項目です」という自然な日本語で表示されるようになります。
これによりエラーメッセージが日本語化され、新規登録画面が完成します。
ログイン状態を支える技術:「セッション」とは
ステートレスな通信を「つなぐ」仕組み
冒頭で触れた通り、Webの通信(HTTP)は状態を持たないステートレスなため、サーバーはアクセスのたびにユーザー情報を忘れてしまいます。同一のユーザーによる操作であることを識別するために使われるのが、「セッション」と「Cookie(クッキー)」です。
Laravelは、サーバー側で管理する「セッション」と、ブラウザ側に保存する「Cookie」という2つの技術を組み合わせて、以下のような流れでユーザーを識別しています。
セッションIDでユーザーを特定する流れ
以下の図は、ユーザーがログインしてから2回目のアクセスを行うまでのデータの流れを表したものです。

ログイン処理
-
ログイン情報を送信
ユーザーがログインフォームからメールアドレスとパスワードを送信します。 -
セッションIDを発行・保存
認証に成功すると、サーバーはランダムな文字列(セッションID)を発行し、情報をサーバー側(データベースやファイル)に記録します。 -
レスポンス + Set-Cookie
サーバーはブラウザに対し、「次からはこのIDを見せてね」と、セッションIDをCookieに保存するよう指示します。
2回目以降のアクセス
-
ページ要求 + Cookie
ブラウザは次回アクセス時に保存していたセッションIDを自動的にサーバーへ送信します。これにより、メールアドレスやパスワードを再度送信する必要はありません。 -
IDを照合
サーバーは送られてきたセッションIDをもとに手元のデータを検索し、「このIDを持っているのは鈴木さんだ」などと特定をします。 -
画面の表示
特定したユーザー専用の画面(マイページなど)を返します。
このように、セッションIDという「一時的な会員証」を行き来させることで、安全かつスムーズに「ログイン状態」を維持しているのです。
ログイン機能
バリデーションの実装
アカウント新規登録と同様、まずはバリデーションを実装します。コマンドでファイルを生成し、以下のコードを記述してください。
php artisan make:request Auth/AuthRequest
//app/Http/Requests/Auth/AuthRequest;
class AuthRequest extends FormRequest
{
/**
* Determine if the user is authorized to make this request.
*/
public function authorize(): bool
{
return true;
}
/**
* Get the validation rules that apply to the request.
*
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
*/
public function rules(): array
{
return [
'email' => ['required', 'string', 'email', 'max:255'],
'password' => ['required', 'string'],
];
}
}
新規登録機能と同様にフォームリクエストを作成して、バリデーションを定義します。
日本語化の設定はすでに完了しているため、これだけでエラーメッセージも自動的に日本語で表示されます。
ログイン処理の実装(Controller)
ユーザーが入力したメールアドレスとパスワードを受け取り、データベースに登録されている情報と一致するかを検証します。Laravelの認証機能(Authファサード)は、パスワードのハッシュ化を含めて、この検証を安全に行ってくれることを示します。
//App/Http/Controllers/Auth/AuthController
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\Auth\LoginRequest;
class AuthController extends Controller
{
public function showLogin()
{
return view('auth.login');
}
public function login(LoginRequest $request)
{
// バリデーション済みのデータを取得
$credentials = $request->validated();
if (Auth::attempt($credentials)) {
$request->session()->regenerate();
return redirect()->intended('/posts');
}
return back()->withErrors([
'email' => 'メールアドレスまたはパスワードが正しくありません。',
])->onlyInput('email');
}
}
このコードで特に重要なのがAuth::attempt()メソッドです。これ一つでデータベースからのユーザー検索とパスワードのハッシュ化&照合を自動的に行ってくれるため、自前で複雑なロジックを書く必要はありません。
セキュリティ対策として重要なのが、$request->session()->regenerate()です。ログイン成功時にセッションIDを新しく発行し直すことで、セッションハイジャック(乗っ取り)を防ぐことができます。
最後に、intended('posts')というメソッドを使っています。「ログインが必要なページ」にアクセスしようとしてログイン画面に飛ばされたユーザーを、ログイン成功後に「元々見たかったページ」へ自動的に戻してくれる便利な機能です(履歴がない場合はpostsへ遷移します)。
このまま、ルーティングまで書いちゃいましょう。
//routes/web.php
// ログイン機能
Route::get('/login', [App\Http\Controllers\Auth\AuthController::class, 'showLogin'])->name('login');
Route::post('/login', [App\Http\Controllers\Auth\AuthController::class, 'login']);
ログインフォームの作成(View)
メールアドレスとパスワードを入力する、シンプルなログイン画面を作成します。
resources/views/auth/login.blade.phpを作成し、以下のコードを記述してください。
//resources/views/Auth/login.blade.php
@extends('layouts.auth')
@section('title', 'ログイン')
@section('content')
<div class="login-container">
@if(session('error'))
<div class="alert alert-danger">{{ session('error') }}</div>
@endif
<form method="POST" action="{{ route('login') }}">
@csrf
<div class="form-group">
<label for="email">メールアドレス</label>
<input type="email"
name="email"
id="email"
value="{{ old('email') }}"
class="form-control @error('email') is-invalid @enderror">
@error('email')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<div class="form-group">
<label for="password">パスワード</label>
<input type="password"
name="password"
id="password"
class="form-control @error('password') is-invalid @enderror">
@error('password')
<span class="invalid-feedback">{{ $message }}</span>
@enderror
</div>
<button type="submit" class="btn btn-primary">ログイン</button>
</form>
</div>
@endsection
ログイン機能が完成しました。
ログアウトの実装
最後にログアウト機能の実装です。ログアウトではユーザーからのデータ入力はないため、バリデーションは必要ありません。AuthControllerにメソッドを追加しましょう。
//App/Http/Controllers/Auth/AuthController
public function logout(Request $request)
{
Auth::logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect()->route('login');
}
まずAuth::logout()を実行することで、セッションに保存されていた「ユーザーID」などの認証データをクリアします。これによりシステム上は未ログイン状態に戻ります。
しかし、セキュリティを完璧にするにはこれだけでは不十分です。続けてsession()->invalidate()で古いセッションID自体を無効化して破棄し、session()->regenerateToken()でCSRFトークンを作り直します。
これらをセットで行うことで、セッション固定攻撃やログアウト直後の隙を狙った攻撃を確実に防ぎ、安全にログイン画面へ戻れるようにしています。
そのまま、ルーティングに反映しましょう。
//routes/web.php
// ログアウト機能
Route::post('/logout', [App\Http\Controllers\Auth\AuthController::class, 'logout'])->name('logout');
これで、認証にまつわる一連の機能(登録・ログイン・ログアウト)がすべて完成しました!
まとめ
今回はWebアプリケーションの根幹をなす認証機能に焦点を当て、その実装方法と背景にある技術を学びました。
LaravelのAuthファサードやFormRequestなどの機能を使うことで、複雑になりがちな認証機能もシンプルに実装できることを体感できたのではないでしょうか。
次回はいよいよ、認証機能を使った投稿・表示機能(CRUD処理)を実装していきます。
最後までお読みいただき、ありがとうございました!