初書:2021/09/13
mac : 11.5.2
php:v8.0.6
laravel:v8.57.0
前書き
ユーザーのログインページとは別に、記事の作成やユーザーの管理などが出来る管理者ページを作ってみる。
laravel-adminというライブラリはあったものの、最新版に公式対応していないっぽいので、今回は普通にlaravelの機能で考える
前提
- laravelの初期ページが表示できる環境とその準備
目標
以下の項目の作成
- データベース
- ログインシステム
- ログインページ
- 簡易的なダッシュボード
- アカウント登録システム
- アカウント登録ページ
以下はこの記事ではやらない
- パスワードの変更/リセット
- メール認証/送信
- adminアカウントの管理ページ
データベース周りの作成
今後この先1つのアカウントで管理することが確約されているのであれば、envファイルにでも認証情報を書けばいいが、
そんなことはないと思うので管理者アカウントを管理するデータベーステーブルを作成する。
今回は通常のユーザーデータがusersというテーブル名なので、admin_usersというテーブルを作成する
Migrationの作成
% php artisan make:migration create_admin_users_table
成功すれば、database/migrations/2021_MM_dd_hhmmss_create_admin_users_table.php
(ファイル名の日付はその時次第)が出来ているはず
出来ていれば、テーブルを作成するための列を作成する
public function up()
{
Schema::create('admin_users', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('email')->unique();
$table->timestamp('email_verified_at')->nullable();
$table->string('password');
$table->integer('admin_level');
$table->rememberToken();
$table->timestamps();
});
}
今回はuserテーブルを真似て作成。admin_level
というintegerが存在するが、これは管理アカウントのアクセスレベルを予定している。
0でログイン不可、100で全ての権限持ち…とか、2進数で可否を載せるとか…?
これができればデータベース内にテーブルを作成する
% php artisan migrate
Modelの作成
次に、操作しやすくするためのModelを作成する
% php artisan make:model AdminUser
app/Models/AdminUser.php
ができるので、中身を書き換える。
なお、login周りのModelは、Illuminate\Contracts\Auth\Authenticatable
で実装する必要があるらしいので、敬称など一部も追加変更する
use Illuminate\Foundation\Auth\User as Authenticatable;
class AdminUser extends Authenticatable
{
use HasFactory;
protected $table = 'admin_users';
protected $fillable = [
'name',
'email',
'password',
'admin_level',
];
protected $hidden = [
'password',
'remember_token',
];
protected $casts = [
'email_verified_at' => 'datetime',
];
}
User.php
とほぼ同じです。
先ほど足したadmin_level
と$table
だけ付け足し。
これは個人的メモ:
fillable
は配列による複数同時に変更してもいい列の一覧。
[Laravel 5.7] Eloquent Modelのfillableとguardedの違い - Qiita
hidden
はModelを使って取得する際に一覧として出力させない列。
casts
は出力する際にstringではなくそれぞれの型に変換してから取得する。
providerの追加
普通のログインと同じような感じで認証させるために、providerとguardを記述する。
これはどちらもconfig/auth.php
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\Models\User::class,
],
'admin_users' => [ // ここ追加
'driver' => 'eloquent',
'model' => App\Models\AdminUser::class, //make:modelしたクラス名
],
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [ // ここ追加
'driver' => 'session',
'provider' => 'admin_users', // providerに追加した名前
],
],
ちなみに両方を簡単に説明すると、
provider
がどの方法でユーザー情報を管理しているかを指定、
guards
がAuth
ファザードで呼び出すための設定をする、感じ。
いい感じに分かりやすく解説してくれてるところ→Laravel の Guard(認証) って実際何をやっているのじゃ? - Qiita
ログインページ周りの作成
データベース周りの作成が終わったので、次はログインを行う画面を作成していく。
コントローラーを作成
まずは、ログイン処理を行うコントローラーを作成する。
もしJetstreamなどを先に使用している場合は、LoginControllerは既に存在しているかもしれない。
% php artisan make:controller LoginController
app/Http/Controllers/LoginController.php
が出来るので、この中身を編集していく。
/**
* 認証の試行を処理
*
* @param \Illuminate\Http\Request $request
* @return \Illuminate\Http\Response
*/
public function adminLogin(Request $request)
{
$credentials = $request->validate([ // 入力内容のチェック
'email' => ['required', 'email'],
'password' => ['required'],
]);
if (Auth::guard('admin')->attempt($credentials)) { // ログイン試行
if ($request->user('admin')?->admin_level > 0) { // 管理権限レベルが0でないか
$request->session()->regenerate(); // セッション更新
return redirect()->intended('admin/dashboard'); // ダッシュボードへ
} else {
Auth::guard('admin')->logout(); // if文でログインしてしまっているので、ログアウトさせる
$request->session()->regenerate(); // セッション更新
return back()->withErrors([ // 権限レベルが0の場合
'error' => 'You do not have permission to log in.',
]);
}
}
return back()->withErrors([ // ログインに失敗した場合
'error' => 'The provided credentials do not match our records.',
]);
}
intelephense使ってるとattemptでエラー出るが、間違えてはいない。
入力内容をチェックしてログインを行う。
ログイン後に管理レベルのチェックを入れ、もし権限が与えられていない場合はログインさせないようにしている。
blade.phpの作成
次にいよいよ表示の方を作成する。今回はadminLogin.blade.php
というadmin専用のログインフォームを作成する。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Laravel</title>
</head>
<body>
@if ($errors->any()) {{-- エラーがあれば出力する --}}
@foreach ($errors->all() as $error)
<div>{{ $error }}</div>
@endforeach
@endif
<form method="POST" action="{{ route("admin.login") }}"> {{-- routeはここと同じ --}}
@csrf
<label for="email">Mail</label>
<input type="text" id="email" name="email">
<label for="password">Password</label>
<input type="password" id="password" name="password">
<button type="submit">Login</button>
</form>
</body>
</html>
css一切入ってない非常にシンプルなForm。
でもログインページ作成のところでcss混ぜられたら見にくくないですか?その装飾おそらく使わないのに。
web.phpに記述
あとはログインページにアクセスできるようにroutes/web.php
を書き換える。
Route::get('/admin/login', function () {
return view('adminLogin'); // blade.php
});
Route::post('/admin/login', [\App\Http\Controllers\LoginController::class, 'adminLogin'])->name('admin.login');
getとpostで動作が若干変わるため、それぞれを記述している。
また、postの方では先ほど作ったコントローラー内の関数を呼び出し、
またadminLogin.blade.php
内でroute
を使って呼び出せるように、name
を振った。
一度動作確認する
ここまでできれば、一度動作しているか確認してみる。
% php artisan serve
これでhttp://127.0.0.1:8000/admin/login
にログインし、とてもシンプルなログイン画面が表示されるはず。
また実際にログインし、(アカウント情報は存在していないため)The provided credentials do not match our records.
と表示されれば成功。
テストアカウントの発行
実際にログインを試みるため、テストアカウントを作成する。
今回はseederを使ってみる。
% php artisan make:seeder AdminUserSeeder
実行すると、database/seeders/AdminUserSeeder.php
ができるので、これを編集する。
public function run()
{
DB::table('admin_users')->insert([
'name' => 'owner',
'email' => 'owner@example.com',
'password' => Hash::make('password'),
'admin_level' => 1,
]);
DB::table('admin_users')->insert([
'name' => 'sub',
'email' => 'sub@example.com',
'password' => Hash::make('password'),
'admin_level' => 0,
]);
}
そしたら、データを挿入するために以下を実行する。
% php artisan db:seed --class=AdminUserSeeder
再度動作確認
再びサーバーを立ててみて、実際にログインできるか確認する。
この記事通りに作成していた場合は、admin_level
が0の場合はログインできないので、それをチェックするため、先にsubのログインを試みる。
(ログアウトの処理を入れていないため)
sub@example.com
でログインした時、You do not have permission to log in.
と表示されれば成功。
またowner@example.com
でログインした時、admin/dashboard
に移動すれば成功。
…いや、成功していなかった。ログインしていてもログインページにアクセスできる。
ログイン済みのリダイレクト
ログインしている時はログイン画面ではなく、先程のadmin/dashboard
に飛ぶようにする。
ミドルウェアの追加
まずはapp/Http/Controllers/LoginController.php
に追記する。
public function __construct()
{
$this->middleware('guest:admin')->except('adminLogout');
}
簡単に説明すると、コントローラーのコンストラクタは、関数を呼び出す前に呼び出されるため、そこでミドルウェア1を指定する。
今回は、guest
ミドルウェアを呼び出し、引数としてadmin
を渡す。
except
は例外。これを指定した場合は、ミドルウェアを通過しない。
後にログアウト画面を作成するため、先にそれを除外しておく。
次に、routes/web.php
で先ほど書いたコードに追記する。
Route::get('/admin/login', function () {
return view('adminLogin');
})->middleware('guest:admin'); // ここ
これは、ログイン時にpostする際は上記のコンストラクターを挟むが、普通にgetでアクセスする際はコントローラーを使わないので、
ここで別途設定する必要がある。
…というか、postも別にここで設定していいような気がした。もしくはgetの方もコントローラーに追加するか。
リダイレクト先の追加
このままアクセスすると、\home
に飛ばされてしまう。
そのため、admin/dashboard
を設定する。
ミドルウェアのguest
は\App\Http\Middleware\RedirectIfAuthenticated::class
が定義されているため、ここを変更する
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
if ($guard === 'admin') { // 以下3行追記する
return redirect(RouteServiceProvider::ADMIN_HOME);
}
return redirect(RouteServiceProvider::HOME);
}
}
ADMIN_HOME
は定義されていないので、定義する。
public const ADMIN_HOME = '/admin/dashboard';
これで完了なのだが、せっかくなので/admin/dashboard
が記述されているもう一箇所も書き換えておく。
+ use App\Providers\RouteServiceProvider;
// (略)
- return redirect()->intended('admin/dashboard');
+ return redirect()->intended(RouteServiceProvider::ADMIN_HOME);
ちなみにこちらがredirect()->intended()
となっているのは、意図されたリダイレクトだからだそう。
実際にどう処理が変化するのか詳しくは調べれていない。(ので情報求む、と言えば果たして集まるのだろうか…)
これで、無事にリダイレクトされるようになる。
ログアウト処理の追加
ログインテストでログアウトする方法が(キャッシュを消すしか)なかったので、追加する。
public function adminLogout(Request $request)
{
Auth::guard('admin')->logout();
$request->session()->invalidate();
$request->session()->regenerateToken();
return redirect('/admin/login');
}
Route::get('/admin/logout', [\App\Http\Controllers\LoginController::class, 'adminLogout'])->name('admin.logout');
ログインと同じなので特に説明はいいかなーと。
ログアウトの際に、セッションの無効化とCSRFトークンを再生成することが推奨されているので、それをしておく。
逆にログアウトしてもセッションが必要な場合は気をつけないといけない。
これでログアウト画面に行くとログアウトされ、/admin/login
にリダイレクトされる。
ログアウトしました、と表示されるページを別途作ってもいいかもしれない。
ダッシュボードの作成
blade.phpの作成
まずはadminDashboard.blade.php
を作成し、以下を記述
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>ダッシュボード</title>
</head>
<body class="antialiased">
<ul>
<li>
ログイン中:{{ Auth::guard('admin')->user()->name ?? 'undefined' }}
</li>
<li>
<a href="{{ route('admin.logout') }}">
ログアウト
</a>
</li>
</ul>
</body>
</html>
シンプルすぎてリストの使い方を間違えるほど。デザインはお任せします。
簡単に説明すると、Auth::guard('admin')->user()
でログイン情報が取得できるので、そのname
を表示している。
また2段目はログアウトのリンク。
ルート設定
Route::get('/admin/dashboard', function () {
return view('adminDashboard');
})->middleware('auth:admin');
今度はログイン中の時しか表示させないので、ミドルウェアはauth:admin
を使用する。
リダイレクト先の指定
auth
の定義はapp/Http/Middleware/Authenticate.php
にあるので、ここを変更する
protected function redirectTo($request) // ※このコードは後に使わない
{
if (! $request->expectsJson()) {
return url('/');
}
}
ログインしていなければ/
に飛びます。
普通であればadmin/login
を指定すればいいのだが、一つ大きな問題がある。
Authenticate.php
のredirectTo
は、guard
の情報を取得できない。
RedirectIfAuthenticated.php
の時のように、引数に$guards
を指定できればいいのだが、
関数名がhandleではない時点でお察しの通り、このクラスは継承元のIlluminate\Auth\Middleware\Authenticate
でhandleが定義されている。
そして、ここでは定義されている$guard
が、何故かredirectToに届いていない。
そのため、リダイレクトを分岐させることができなかった。
と、書いてて思いついたのだが、その前のコードも上書きしてしまえばいいのでは?
protected function unauthenticated($request, array $guards)
{
throw new AuthenticationException(
'Unauthenticated.',
$guards,
$this->redirectToOriginal($request, $guards)
);
}
protected function redirectToOriginal($request, array $guards)
{
foreach ($guards as $guard) {
if ($guard === 'admin') {
return route('admin.login');
}
}
}
いけた。redirectTo
は継承の都合上書き換えられないので、redirectToOriginal
を使用する。
これで、ログインしていない時にダッシュボードを踏むと、admin/login
に飛ばされる。
アカウント登録画面の作成
長くないですか?そろそろ疲れてきた。
でももう少し続きます。
次はアカウントを登録する画面とその仕組みを作成する。
管理画面のアカウントなので、一般公開する必要もないかなと思い、今回はログイン済みのアカウントが新規にアカウントを発行する仕組みを作る。
コントローラーの作成
データベースの登録とかを行うコントローラー。
% php artisan make:controller RegisterController
次に中身を書いていく。
// 前略
use Illuminate\Support\Facades\Validator;
use App\Models\AdminUser;
use Illuminate\Validation\Rules\Password;
use Illuminate\Foundation\Auth\RegistersUsers;
class RegisterController extends Controller
{
use RegistersUsers;
public function adminRegisterForm(Request $request)
{
return view('adminRegister');
}
protected function adminValidator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:App\Models\AdminUser'],
'password' => ['required', 'confirmed', Password::min(8)],
'admin_level' => ['required', 'numeric'],
]);
}
protected function adminRegisterDatabase(array $data)
{
return AdminUser::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
'admin_level' => $data['admin_level'],
]);
}
public function adminRegister(Request $request)
{
$this->adminValidator($request->all())->validate();
$user = $this->adminRegisterDatabase($request->all());
if ($user) {
return view('adminRegister', ['registered' => true, 'registered_email' => $user->email]);
}
}
// 後略
唐突の関数が連続。
まあ一つでも書けるが、結構分割してるサイトが多かったので見習って分割。あと1関数1作業(?)とか言うし。
adminRegisterForm
はblade.phpを表示。ログインの時はweb.phpに直接記述したが、今回はこちらに書いてみる。
adminValidator
はValidatorの作成。
adminRegisterDatabase
はデータベースに挿入する。
adminRegister
がリクエストを受け付ける場所で、上記2つの関数を使ってデータを登録作業を行なっている。
あと成功したら成功したよーと表示する。
blade.phpの作成
登録画面。
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>登録</title>
</head>
<body class="antialiased">
@if ($errors->any())
@foreach ($errors->all() as $error)
<div>{{ $error }}</div>
@endforeach
@endif
@isset($registered)
<div>登録に成功しました。メールアドレス:{{ $registered_email }}</div>
@endisset
<form method="POST" action="{{ route('admin.register') }}">
@csrf
<div>
<label for="name">Name</label>
<input type="text" id="name" name="name">
</div>
<div>
<label for="email">Mail</label>
<input type="text" id="email" name="email">
</div>
<div>
<label for="password">Password</label>
<input type="password" id="password" name="password">
</div>
<div>
<label for="password_confirmation">Password(confirmed)</label>
<input type="password" id="password_confirmation" name="password_confirmation">
</div>
<div>
<label for="admin_level">AdminLevel</label>
<input type="text" id="admin_level" name="admin_level">
</div>
<div>
<button type="submit">Register</button>
</div>
</form>
</body>
</html>
項目が多かったのでdivで囲んだ。
先程のadminValidator
でpasswordにconfirmed
を設定しているため、同名のconfirmed
を設定する必要がある。
あとAdminLevel
は記事内に定期的に出てくるが、これはご自由にお使いください。2
ルート設定
Route::get('/admin/register', [\App\Http\Controllers\RegisterController::class, 'adminRegisterForm'])->middleware('auth:admin');
Route::post('/admin/register', [\App\Http\Controllers\RegisterController::class, 'adminRegister'])->middleware('auth:admin')->name('admin.register');
今回は、ログイン済みのアカウントで新規アカウントを作成するので、middleware
はauth
を選択する。
誰でも登録できる時は、ログインと同じでguest
にする。
また、adminDashboard.blade.php
にアカウント作成ページへ行けるリンクを追加する。
<li>
<a href="{{route('admin.register')}}">
アカウント作成
</a>
</li>
再度動作確認
admin/register
にアクセスし、
- アカウントを新規作成できるか
- 作成したアカウントでログインできるか
など、今回作成したものが一通り動くかを確認すればok。
終わりに
やりたいこと一つで、一通り周りの動き方を理解しないといけないので、
最初から全体的に把握してないと作れないなあと。
逆にいえば結構laravelの動作が分かってきたような気もする。
今回はここで一旦切るが、
- メールの確認
- パスワードの変更/リセット
- アカウントの管理
等が出来ていないので、その辺も後に書いていくかも。
もし変なところや間違えているところ、改善できる場所があれば教えてください。
逆に合っている場合も合ってると教えてくれると非常に助かります…!3
では!
参考サイト
Laravel 8でマルチ認証して一般ログインと管理ログインを分ける方法(UI方式、非Jetstream、非Fortify、非Breeze) - Qiita
laravel日本語訳サイトより
Eloquentの準備 8.x Laravel
認証 8.x Laravel
ミドルウェア 8.x Laravel
フォームで
[Laravel 5.7] フォーム認証処理の仕組み - Qiita