この記事はアクシス Advent Calendar 2020 1日目です。
株式会社アクシスの渋谷オフィスdev-campの成果を基に作成しています。
はじめに
Laravel8では、今までのlaravel/uiに代わる公式の認証パッケージとしてJetstreamが導入されました。
laravel/uiが使えなくなったわけじゃないですが、Jetstreamの方が公式の推しっぽい。認証のドキュメントで紹介されてるのもJetstreamですし。
とはいえこのJetstream、とりあえずで使うにはあまりにも全部盛りが過ぎるためか、Laravelの中の人が新たに認証パッケージを開発していて、それがlaravel/breezeです。
ただlaravel/breezeはデフォルトではUserのログインしかできないので、AdminとUserの2種類でログインできるようにしてみよう、というのがこの記事の主旨です。
※ ひとまず2種類のユーザーでログインできるところまでで、メール認証やパスワード再設定とかまでは手が回りませんでした。
laravel/breezeについてもう少し補足
- vueやreact不使用。controllerとbladeだけ使って、普通のwebアプリとして動きます。私のようなフロントエンドよくわからんおじさんでも安心。
- ただしtailwind cssを使うのでnodeは必要です。laravel-mixが介護してくれるので頑張りましょう。
- controllerやbladeは全部プロジェクト内の
app
やresources
にコピーされるので、カスタマイズ自由。vendor
の中にコードがあって手出しできない…みたいなことにはなりません。 - Jetstream(気流), tailwind(追い風), breeze(そよ風)となんか風関連のネーミングでまとめてますね。Laravel界隈はそういう機運なのか。
構成
環境構築は大体docker-composeを使いますが、nodeだけはasdfを使ってホストPCにローカルインストール。
- macOS Catalina 10.15.7
- node v14.14.0
- docker desktop 2.5.0.1
- php:7.4-fpm-alpine
- nginx:1.18.0
- composer:2
- redis:5.0.7 セッションに使うだけです
- mailhog:1.0.1 smtpサーバとして使うつもりでしたが今回は不要というか、そこまでできなかった…
- Laravel 8.16.1
- laravel/breeze 0.1.0
セットアップ
Laravel本体のインストール
$ composer create-project --prefer-dist laravel/laravel [何か適当にプロジェクト名]
laravel/breezeインストール
リポジトリのREADMEに従って
$ composer composer require laravel/breeze --dev
$ php artisan breeze:install
ここまででプロジェクト内にディレクトリが作られ、controllerやresources, routeなどのファイルが自動生成されます。こんな感じの構成に。
breeze:install
完了後は以下のメッセージが出るので
Breeze scaffolding installed successfully.
Please execute the "npm install && npm run dev" command to build your assets.
言われたとおりにnpm実行。
laravel/breeze
にlaravel-mix
が同梱されてるので、コマンド叩くだけで動きます。
$ npm install && npm run dev
完了したらhttp://localhost:8231
を開いてみましょう。
ページ右上にLogin
, register
が出てきましたね。
HelloWorld的なやつはここで終わりです。
以降はカスタマイズしてマルチログインを実装していきます。
Adminユーザー作成
管理画面にログインするユーザーを想定したAdminモデルを作成します。-m
でマイグレーションも一緒に。
$ php artisan make:model AdminModel -m
モデルクラスに認証機能を持たせるため、継承元を変更します。
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Notifications\Notifiable;
class Admin extends Authenticatable # ModelからAuthenticatableに変更
{
use HasFactory, Notifiable;
protected $guard = 'admins';
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
}
続いてマイグレーション作成。users
テーブルのマイグレーションファイルを丸パクリです。
public function up()
{
Schema::create('admins', 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();
$table->softDeletes();
});
}
マイグレーションを実行します。
$ app php artisan migrate
これでAdminユーザーを扱うテーブルとモデルクラスが出来上がりました。
続いてルート設定です。
ルート設定
routes/
に設定ファイル作成
前述のbreeze:install
でルート設定ファイルも設置されています。
routes/auth.php
を見てみましょう。結構な行数があるのでコードの貼り付けはしません。カスタマイズ前のコードを見たい方はリポジトリのコードをどうぞ。
やはりルート設定もUserだけがログインする想定ですので、Adminのルートを追加します。
routes/web.php
, routes/auth.php
は削除し、routes/user.php
, routes/admin.php
を作成しています。
実際のコードは↓をご覧ください。web.phpとauth.phpをマージしたような内容です。
なおprefixはここでは設定しません。次項で説明します。
ルートプロバイダ
app/Providers/RouteServiceProvider.php
を編集します。
laravel/breezeが設定しているHOME
がユーザー向けのリダイレクト先なので、AdminユーザーのホームURLとなる定数ADMIN_HOME
を設定します。
public const HOME = '/dashboard';
public const ADMIN_HOME = '/admin/dashboard'; // 追記
続いてboot()
メソッドにUserとAdminのルートを設定します。
example.com/
でUser、example.com/admin
はAdminユーザーがアクセスする領域を想定し、prefix
を設定します。
またas()
で名前をつけることでルート名にもprefixがつき、route()
の引数を見るだけで何のルートを出力するのか分かりやすくします。
public function boot()
{
$this->configureRateLimiting();
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::prefix('/')
->as('user.')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/user.php'));
Route::prefix('admin')
->as('admin.')
->middleware('web')
->namespace($this->namespace)
->group(base_path('routes/admin.php'));
});
}
これでルート設定は完了しました。
次はGuard設定です。
Guard設定
Laravel標準の認証用機能であるGuardを使用します。
Guardについての説明はそれだけで1本の記事になってしまうので、参考リンクの紹介にとどめます。
ただ、今回のカスタマイズで「なぜそうするのか」の理解につながるので、一読をおすすめします。
- Laravel 8.x 認証 > 自前のユーザー認証
- Laravel の Guard(認証) って実際何をやっているのじゃ?
ではconfig/auth.php
を編集しましょう。変更箇所だけ抜粋します。
'defaults' => [
'guard' => 'users', // ここ変更
'passwords' => 'users',
],
'guards' => [
// 追記ここから
'users' => [
'driver' => 'session',
'provider' => 'users',
],
'admins' => [
'driver' => 'session',
'provider' => 'admins',
],
// ここまで
'api' => [
'driver' => 'token',
'provider' => 'users',
'hash' => false,
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
// adminsを追加
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
// adminsを追加
'admins' => [
'provider' => 'admins',
'table' => 'password_resets',
'expire' => 60,
'throttle' => 60,
],
],
Guard設定追加完了。まだまだ続きます。
Middleware設定
Authenticate
アクセスしたユーザーが未認証の場合のリダイレクト処理を書きます。全体的に変えてます。
use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Support\Facades\Route;
class Authenticate extends Middleware
{
protected string $user_route = 'user.login';
protected string $admin_route = 'admin.login';
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
if (Route::is('user.*')) {
return route($this->user_route);
} elseif (Route::is('admin.*')) {
return route($this->admin_route);
}
}
}
}
RedirectIfAuthenticated
こちらは「ログイン済みのユーザーがアクセスしてきたらリダイレクトする」処理を書きます。
例えばログイン済みユーザーがログインフォームにアクセスした時とかですね。
リダイレクト先のURLは先述のルートプロバイダで定義したものです。
class RedirectIfAuthenticated
{
private const GUARD_USER = 'users';
private const GUARD_ADMIN = 'admins';
public function handle(Request $request, Closure $next, ...$guards)
{
if (Auth::guard(self::GUARD_USER)->check() && $request->routeIs('user.*')) {
return redirect(RouteServiceProvider::HOME);
}
if (Auth::guard(self::GUARD_ADMIN)->check() && $request->routeIs('admin.*')) {
return redirect(RouteServiceProvider::ADMIN_HOME);
}
return $next($request);
}
}
ミドルウェアはこの2つのみで、次はリクエストクラスです。
だいぶ長いですね、ダレてきそうです。もうちょっとお付き合いください。
リクエストクラス
ログインフォームで入力した値を処理するapp/Http/Requests/Auth/LoginRequest.php
を編集します。
Admin用とUser用のどちらのフォームからログインしてきたかを判別して、Auth::guard()
に渡しています。
public function authenticate()
{
$this->ensureIsNotRateLimited();
// 追加ここから
if ($this->routeIs('admin.*')) {
$guard = 'admins';
} else {
$guard = 'users';
}
if (! Auth::guard($guard)->attempt($this->only('email', 'password'), $this->filled('remember'))) {
// 追加ここまで
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => __('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
もうちょっとだけ続くんじゃ。
Admin用のcontrollerやbladeを作成する
ファイル複製
UserとAdminとでディレクトリを分けてcontrollerを作成します。bladeも同様。
breezeがインストールしたファイルをがさっとコピーして、User用とAdmin用とに分けます。
- controller
-
app/Http/Controllers/Auth
→app/Http/Controllers/User/Auth
にディレクトリごと移動 -
app/Http/Controllers/User/Auth
をコピーしてapp/Http/Controllers/Admin/Auth
にディレクトリごとリネーム
-
- view
-
resources/views/auth
→resources/views/user/auth
にディレクトリごと移動 -
resources/views/user/auth
をコピーしてresources/views/admin/auth
にディレクトリごとリネーム
-
コード修正
元々はroute()
の引数がlogin
とかregister
みたいになってますが、ルート設定の作業を経てadmin.login
やuser.dashboard
といったルート名になってますので、それに合わせて修正していきます。
ルート一覧の確認はおなじみのphp artisan route:list
で。 | grep "admin."
とかやれば、adminだけのルート一覧表示とかできます。
ログイン/ログアウト処理も少し手を入れます。
たとえばAdminのログインはApp\Http\Controllers\User\Auth\AuthenticatedSessionController::store()
ですが、リダイレクト先がRouteServiceProvider::HOME
になってるので、これをRouteServiceProvider::ADMIN_HOME
に直したり。
一方でログアウト処理は同クラスのdestroy()
ですが、ここで呼び出してるAuth::logout()
も、まずguardを指定してからログアウトします。
指定しないと「AdminでログアウトしたのにUserの方でログアウトした」みたいなことになります。
具体的には以下のようなコードに。
public function destroy(Request $request)
{
// guardを指定してログアウト
Auth::guard('admins')->logout();
// invalidate()でセッションデータ全体が破棄されてしまうのでコメントアウト
//$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/');
}
パスワードリセットとかメールアドレス確認の処理には今回手を入れてないんですが、guardの指定はたぶんAuth::***()
的なコード全部が対象になるんじゃないでしょうか。まずAuth::guard('admins')
してから->validate()
とか->login()
とか呼び出す感じで。
で、出来上がったものが以下になります。これも結構な量になるのでgithubをご覧ください。
viewはcomponentsやlayoutなどの共通パーツはそのままで、各ページのメインコンテンツのbladeを分けた感じ。
作業は以上です。おつかれさまでした。
このカスタマイズで、ひとまずログイン/ログアウト部分はマルチユーザーに対応できました。
さいごに
laravel/breezeは本当に最低限のスキャフォールドしかせず、あとはお好きなようにって感じですね。
guardとかの理解さえすれば、柔軟に対応できていいんじゃないでしょうか。そもそもLaravel標準の認証はguardだし。
私も今回の記事を書くにあたって、今まで何となく使ってたAuth::user()
が何してるのかとか理解できてよかったです。
あと、controllerとbladeで全部解決してるので、vueやreactがよく分からない古のwebアプリおじさんにも安心。
ところで私はPHP開発にPHPStormを使ってますが、今回laravel ideaをお試し導入しました。ルート名やviewファイルの入力補完とかしてくれて、地味に便利。30日トライアルだけど買っちゃうか…