本記事のゴール
開発環境
- M1 MacbookPro
- macOS Venture13.5.1
- Docker Deskctop
利用フレームワーク/パッケージ
- Laravel Sail
- Laravel 10
- Laravel Breeze
実装フロー
-
プロジェクトの初期設定
1-1. プロジェクト作成
1-2. エイリアスの設定
1-3. phpMyAdminの導入
1-4. 起動とパッケージインストール
1-5. migrateの実行
1-6. タイムゾーンの変更
1-7. mysqlの設定 -
テーブル設定・初期ルーティング
2-1. migrationファイルを編集
2-2. モデルファイルを修正
2-3. welcomeページを修正
2-4. bladeファイルを作成
2-5. 初期ルーティング設定
2-6. コントローラー設定 -
メール送信のロジック作成
3-1. Mailableクラスを作成
3-2. コントローラーの編集 -
ワンタイムトークン認証のロジック作成
4-1. トークンの正当性チェック
4-2. ルーティングを追加
1.プロジェクトの初期設定
Laravel Breezeのインストールまですでに終わっている方は
2-1. migrationファイルを編集こちらまでスキップしてください。
1-1.プロジェクト作成
任意の場所にプロジェクトを作成します。
cd dev
mkdir docker
cd docker
以下のコマンドでLaravelのプロジェクトを作成します
curl -s https://laravel.build/[任意のプロジェクト名] | bash
私はLaravel-Qiita
というプロジェクト名で作成しました。
curl -s https://laravel.build/laravel-qiita | bash
1-2.エイリアスの設定
すでに設定してある方は無視してください。
alias sail='[ -f sail ] && bash sail || bash vendor/bin/sail'
1-3.phpmyadminの導入
プロジェクト内に移動しdocker-compose.yml
に追記します
services:
laravel.test:
・・・省略
phpmyadmin:
image: phpmyadmin/phpmyadmin
links:
- mysql:mysql
ports:
- 8080:80
environment:
MYSQL_USER: "${DB_USERNAME}"
MYSQL_PASSWORD: "${DB_PASSWORD}"
PMA_HOST: mysql
networks:
- sail
1-4.起動とパッケージインストール
Sailの起動
sail up
これを実行しlocalhost
にアクセスすると初期ページが表示されます。
パッケージインストール
sail npm install
ユーザ認証パッケージにLaravel Breeze
を使用します。
sail composer require laravel/breeze --dev
sail artisan breeze:install
バージョンによって色々聞かれるみたいですがすべてデフォルトで進めました。(参考までに↓)
js/cssのビルド
sail npm run dev
1-5.migrateの実行
sail artisan migrate
1-6.タイムゾーンの変更
ワンタイムトークンの有効期限の設定のため、日本時間に変更します。
'timezone' => 'Asia/Tokyo',
1-7.mysqlの設定
dbアクセスの際にエラーになる場合があるためstrictモードをオフにします。
'mysql' => [
'driver' => 'mysql',
'url' => env('DATABASE_URL'),
'host' => env('DB_HOST', '127.0.0.1'),
'port' => env('DB_PORT', '3306'),
'database' => env('DB_DATABASE', 'forge'),
'username' => env('DB_USERNAME', 'forge'),
'password' => env('DB_PASSWORD', ''),
'unix_socket' => env('DB_SOCKET', ''),
'charset' => 'utf8mb4',
'collation' => 'utf8mb4_unicode_ci',
'prefix' => '',
'prefix_indexes' => true,
'strict' => false, //変更
'engine' => null,
'options' => extension_loaded('pdo_mysql') ? array_filter([
PDO::MYSQL_ATTR_SSL_CA => env('MYSQL_ATTR_SSL_CA'),
]) : [],
],
2.テーブル設定・初期ルーティング
2-1.migrationファイルを編集
カラムの追加処理を追記します。
sail artisan make:migration add_onetime_token_to_users_table --table=users
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
/**
* Run the migrations.
*/
public function up(): void {
Schema::table('users', function (Blueprint $table) {
$table->string('name')->default('未設定')->change(); // nameカラムのnullを許可する
$table->string('password')->nullable()->change(); // passwordカラムのnullを許可する
$table->char("onetime_token", 4)->nullable(); // ワンタイムトークン
$table->dateTime("onetime_expiration")->nullable(); // ワンタイムトークンの有効期限
});
}
/**
* Reverse the migrations.
*/
public function down(): void {
Schema::table('users', function (Blueprint $table) {
$table->string('name')->default()->change();
$table->string('password')->nullable(false)->change();
$table->dropColumn("onetime_token");
$table->dropColumn("onetime_expiration");
});
}
};
実行しておきます。
sail artisan migrate
2-2.モデルファイルを修正
変更可能なカラムにワンタイムトークンとその有効期限を追加します。
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;
class User extends Authenticatable {
use HasApiTokens, HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
'onetime_token', // 追加
'onetime_expiration' // 追加
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
'onetime_token', // 追加
'onetime_expiration' // 追加
];
/**
* The attributes that should be cast.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
2-3.welcomeページを修正
ログイン画面へのリンクを追加します。
<body class="antialiased">
<div class="relative sm:flex sm:justify-center sm:items-center min-h-screen bg-dots-darker bg-center bg-gray-100 dark:bg-dots-lighter dark:bg-gray-900 selection:bg-red-500 selection:text-white">
<div class="sm:fixed sm:top-0 sm:right-0 p-6 text-right z-10">
@auth
<a href="{{ url('/dashboard') }}" class="font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">Dashboard</a>
@else
@if (Route::has('auth.first-auth'))
<a href="{{ route('auth.first-auth') }}" class="ml-4 font-semibold text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-white focus:outline focus:outline-2 focus:rounded-sm focus:outline-red-500">ログイン / 新規登録</a>
@endif
@endauth
</div>
2-4.bladeファイルを作成
メール認証画面
<x-guest-layout>
<form method="POST" action="{{ route('sendTokenEmail') }}">
@csrf
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="email" :value="__('Email')" />
<x-text-input id="email" class="block mt-1 w-full" type="email" name="email" :value="old('email')" required/>
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ml-4">
{{ __('認証コード送信') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>
ワンタイムトークン認証画面
<x-guest-layout>
<form method="POST" action="{{ route('dashboard') }}">
@csrf
<!-- Email Address -->
<div class="mt-4">
<x-input-label for="onetime_token" :value="__('認証コード')" />
<x-text-input id="onetime_token" class="block mt-1 w-full" type="number" name="onetime_token" :value="old('onetime_token')" required/>
<x-input-error :messages="$errors->get('email')" class="mt-2" />
</div>
<div class="flex items-center justify-end mt-4">
<x-primary-button class="ml-4">
{{ __('ログイン / 新規登録') }}
</x-primary-button>
</div>
</form>
</x-guest-layout>
2-5.初期ルーティング設定
一旦確認用にルーティングを設定します。
Route::middleware('guest')->group(function () {
// Route::get('register', [RegisteredUserController::class, 'create'])
// ->name('register');
/**
**最初のメール入力画面を表示するルーティング
*/
Route::get('first-auth', [RegisteredUserController::class, 'create'])
->name('auth.first-auth'); // 追加
/**
**トークンを含んだメールを送信するルーティング
*/
Route::post('sendTokenEmail', [RegisteredUserController::class, 'sendTokenEmail'])
->name('sendTokenEmail');
・・・省略
}
2-6.コントローラー設定
ひとまず画面を表示させるのみです。
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
class RegisteredUserController extends Controller {
/**
**最初の最初のメール入力画面を表示する
*/
public function create(): View {
return view('auth.first-auth');
}
/**
**ワンタイムトークンが含まれるメールを送信する
*/
public function sendTokenEmail(Request $request) {
return view('auth.second-auth');
}
・・・省略
});
ここまで設定するとこのような画面が表示されます。
※撮影のため拡大してあります
3. メール送信のロジック作成
3-1.mailableクラスを作成
sail artisan make:mail TokenEmail
<?php
namespace App\Mail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Mail\Mailable;
use Illuminate\Mail\Mailables\Content;
use Illuminate\Mail\Mailables\Envelope;
use Illuminate\Queue\SerializesModels;
class TokenEmail extends Mailable {
use Queueable, SerializesModels;
private $email;
private $onetime_token;
/**
* Create a new message instance.
*/
public function __construct($email, $onetime_token) {
$this->email = $email;
$this->onetime_token = $onetime_token;
}
/**
**メール作成
*/
public function build() {
return $this->to($this->email)
->subject("認証コード")
->view('auth.mail')
->with([
'onetime_token' => $this->onetime_token
]);
}
}
ついでにメール本文を表示するbladeファイルも作成しておきます
<h2>{{$onetime_token}}</h2>
3-2.コントローラーの編集
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
use App\Mail\TokenEmail;
use Illuminate\Support\Facades\Mail;
class RegisteredUserController extends Controller {
/**
**最初の最初のメール入力画面を表示する
*/
public function create(): View {
return view('auth.first-auth');
}
/**
**引数で渡されたメールアドレスとワンタイムトークンをusersテーブルに追加するコントロール
*/
public static function storeEmailAndToken($email, $onetime_token, $onetime_expiration) {
User::create([
'email' => $email,
'onetime_token' => $onetime_token,
'ontime_expiration' => $onetime_expiration
]);
}
/**
**引数で渡されたワンタイムトークンをusersテーブルに追加するコントロール
*/
public static function storeToken($email, $onetime_token, $onetime_expiration) {
User::where('email', $email)->update([
'onetime_token' => $onetime_token,
'onetime_expiration' => $onetime_expiration
]);
}
/**
**ワンタイムトークンが含まれるメールを送信する
*/
public function sendTokenEmail(Request $request) {
$email = $request->email;
$onetime_token = "";
for ($i = 0; $i < 4; $i++) {
$onetime_token .= strval(rand(0, 9)); // ワンタイムトークン
}
$onetime_expiration = now()->addMinute(3); // 有効期限
$user = User::where('email', $email)->first(); // 受け取ったメールアドレスで検索
if ($user === null) {
RegisteredUserController::storeEmailAndToken($email, $onetime_token, $onetime_expiration);
} else {
RegisteredUserController::storeToken($email, $onetime_token, $onetime_expiration);
}
session()->flash('email', $email); // 認証処理で利用するために一時的に格納
Mail::send(new TokenEmail($email, $onetime_token));
return view("auth.second-auth");
}
}
ここまでできると、first-authの画面で認証コード送信
を押した際に
http://localhost:8025/
にメールが届くようになります。
4.ワンタイムトークンの認証ロジック
ここまできたらもう少しです!
4-1.トークンの正当性チェック
<?php
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use App\Models\User;
use App\Providers\RouteServiceProvider;
use Illuminate\Auth\Events\Registered;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Validation\Rules;
use Illuminate\View\View;
use App\Mail\TokenEmail;
use Carbon\Carbon;
use Illuminate\Support\Facades\Mail;
class RegisteredUserController extends Controller {
・・・省略
/**
**ワンタイムトークンが正しいか確かめてログインさせる
*/
public function auth(Request $request): RedirectResponse {
$user = User::where('email', session('email'))->first();
$expiration = new Carbon($user['onetime_token']);
if ($user['onetime_token'] == $request->onetime_token && $expiration > now()) {
Auth::login($user);
return redirect(RouteServiceProvider::HOME);
}
return redirect()->route('auth.first-auth');
}
}
4-2.ルーティングを追加
<?php
use App\Http\Controllers\Auth\AuthenticatedSessionController;
use App\Http\Controllers\Auth\ConfirmablePasswordController;
use App\Http\Controllers\Auth\EmailVerificationNotificationController;
use App\Http\Controllers\Auth\EmailVerificationPromptController;
use App\Http\Controllers\Auth\NewPasswordController;
use App\Http\Controllers\Auth\PasswordController;
use App\Http\Controllers\Auth\PasswordResetLinkController;
use App\Http\Controllers\Auth\RegisteredUserController;
use App\Http\Controllers\Auth\VerifyEmailController;
use Illuminate\Support\Facades\Route;
Route::middleware('guest')->group(function () {
/**
**最初のメール入力画面を表示するルーティング
*/
Route::get('first-auth', [RegisteredUserController::class, 'create'])
->name('auth.first-auth'); // 追加
/**
**トークンを含んだメールを送信するルーティング
*/
Route::post('sendTokenEmail', [RegisteredUserController::class, 'sendTokenEmail'])
->name('sendTokenEmail');
/**
**ワンタイムトークンが正しいか確かめてログインさせるルーティング
*/
Route::post('login', [RegisteredUserController::class, 'auth'])
->name('login'); // 追加
});
・・・省略
おわりに
とても長い記事となってしまいましたがご覧いただきありがとうございました。
今回はLaravel Breezeの認証機能を改造し、ワンタイムトークンによる認証を作成してみました。
参考にしていただくのは結構ですが、Breezeで用意されているものを変更して使っているため、セキュリティに関しては保証できかねます。
もし少しでも役に立った場合はぜひ♡をお願いします。