Laravel multi auth with subpath using guards

  • August 31, 2023
  • 429

Một website thường có 2 phần admin dành cho người quản trị và user dành cho người dùng. Để đăng nhập vào 2 phần admin hoặc user của một website ta có thể dùng một table users hoặc 2 table usersadmins

Dùng 1 table users trong trường hợp dùng 1 account có thể đăng nhập cả 2 phần admin và user, update password ở user thì ở admin cũng bị update... Dùng 2 table trong trường hợp dùng 2 account khác nhau để đăng nhập admin hoặc user. Thông thường chúng ta sẽ tách biệt account của user và admin nên sẽ dùng 2 bảng.

Để đăng nhập user và admin sử dụng 2 bảng khác nhau chúng ta sẽ sử dụng multi auth với các guard khác nhau. Giờ chúng ta sẽ dùng multi auth với laravel 10 jetstream inertia.

Create admins table

Mặc định laravel đã có bảng users quản lý các user đăng nhập truy cập các chức năng cho user. Giờ chúng ta sẽ tạo bảng admins để quản lý các user đăng nhập truy cập chức năng dành cho admin. Tạo file admin migration bằng command.


php artisan make:migration create_admins_table

<?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::create('admins', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->timestamp('email_verified_at')->nullable();
            $table->tinyInteger('status')->default(1);
            $table->rememberToken();
            $table->string('profile_photo_path', 2048)->nullable();
            $table->timestamps();
        });
    }

    /**
     * Reverse the migrations.
     */
    public function down(): void
    {
        Schema::dropIfExists('admins');
    }
};

Create Admin model

Tạo admin model tương tự User model. Khai báo thêm admin guard.


<?php

namespace App\Models;

use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Jetstream\HasProfilePhoto;
use Laravel\Sanctum\HasApiTokens;
use Laravel\Cashier\Billable;

class Admin extends Authenticatable
{
    use HasApiTokens;
    use HasFactory;
    use HasProfilePhoto;
    use Notifiable;
    use Billable;

    protected $guard = 'admin';

    /**
     * The attributes that are mass assignable.
     *
     * @var array<int, string>
     */
    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    /**
     * The attributes that should be hidden for serialization.
     *
     * @var array<int, string>
     */
    protected $hidden = [
        'password',
        'remember_token',
    ];

    /**
     * The attributes that should be cast.
     *
     * @var array<string, string>
     */
    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    /**
     * The accessors to append to the model's array form.
     *
     * @var array<int, string>
     */
    protected $appends = [
        'profile_photo_url',
    ];
}

Create user seeder

Tạo các account cho user và admin sử dụng seed. Tạo admin factory


<?php

namespace Database\Factories;

use Illuminate\Database\Eloquent\Factories\Factory;
use App\Models\Admin;
use Illuminate\Support\Str;

/**
 * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Model>
 */
class AdminFactory extends Factory
{
    /**
     * The name of the factory's corresponding model.
     *
     * @var string
     */
    protected $model = Admin::class;

    /**
     * Define the model's default state.
     *
     * @return array<string, mixed>
     */
    public function definition(): array
    {
        return [
            'name' => $this->faker->name(),
            'email' => $this->faker->unique()->safeEmail(),
            'email_verified_at' => now(),
            'password' => '$2y$10$92IXUNpkjO0rOQ5byMi.Ye4oKoEa3Ro9llC/.og/at2.uheWG/igi', // password
            'remember_token' => Str::random(10),
            'profile_photo_path' => null,
        ];
    }

    /**
     * Indicate that the model's email address should be unverified.
     */
    public function unverified(): static
    {
        return $this->state(function (array $attributes) {
            return [
                'email_verified_at' => null,
            ];
        });
    }
}

Tạo AdminSeeder và UserSeeder


// AdminSeeder

<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\Admin;
use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;

class AdminSeeder extends Seeder
{
    use WithoutModelEvents;

    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        if (Admin::get()->count() === 0) {
            Admin::factory()->create([
                'name' => 'admin',
                'email' => 'huu@gmail.com',
                'password' => Hash::make('Admin@123'),
                'status' => 1,
            ]);
        }
    }
}

// User seeder
<?php

namespace Database\Seeders;

use Illuminate\Database\Seeder;
use App\Models\User;
use Illuminate\Support\Facades\Hash;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;

class UserSeeder extends Seeder
{
    use WithoutModelEvents;

    /**
     * Run the database seeds.
     */
    public function run(): void
    {
        if (User::get()->count() === 0) {
            User::factory()->create([
                'name' => 'user',
                'email' => 'huu@gmail.com',
                'password' => Hash::make('User@123'),
            ]);
        }
    }
}

// Setting seeder
<?php

namespace Database\Seeders;

// use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use Illuminate\Database\Seeder;

class DatabaseSeeder extends Seeder
{
    /**
     * Seed the application's database.
     */
    public function run(): void
    {
        $this->call([
            UserSeeder::class,
            AdminSeeder::class,
        ]);
    }
}

Chạy command để tạo bảng admins, users và seed các account demo


// Command create db
php artisan migrate

// Command seed data
php artisan db:seed

Config multi auth

Trong laravel authentication sẽ định nghĩa được nhiều guard, mỗi guard tương ứng với một thành phần xác thực khác nhau. định nghĩa các guard trong file config/auth.php giờ chúng ta sẽ tạo 2 guard là admin và user.


'guards' => [
        'web' => [
            'driver' => 'session',
            'provider' => 'users',
        ],
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
    ],

Thiết lập provider cho các guard.


'providers' => [
        'users' => [
            'driver' => 'eloquent',
            'model' => App\Models\User::class,
        ],
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Models\Admin::class,
        ],
    ],

Thiết lập reset password


'passwords' => [
        'users' => [
            'provider' => 'users',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
        'admins' => [
            'provider' => 'admins',
            'table' => 'password_resets',
            'expire' => 60,
            'throttle' => 60,
        ],
    ],

Setting route

Thêm admin route routes/admin.php bằng cách khai báo trong App\Providers\RouteServiceProvider


$this->routes(function () {
            Route::middleware('api')
                ->prefix('api')
                ->group(base_path('routes/api.php'));

            Route::middleware('web')
                ->group(base_path('routes/web.php'));

            Route::middleware('web')
                ->prefix('admin')
                ->group(base_path('routes/admin.php'));
        });

Khai báo route cho user


Route::middleware(['guest'])->group(function () {
    Route::get('/login', [LoginController::class, 'index'])->name('user.login.index');
    Route::post('/login', [LoginController::class, 'store'])->name('user.login.store');
});
Route::group(['middleware' => ['auth']], function () {
    Route::get('/home', [HomeController::class, 'index'])->name('user.home.index');
    Route::get('/logout', [LoginController::class, 'logout'])->name('user.logout');
});

Khai báo route cho admin


<?php

use Illuminate\Foundation\Application;
use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Admin\LoginController;
use App\Http\Controllers\Admin\HomeController;

/*
|--------------------------------------------------------------------------
| Web Routes
|--------------------------------------------------------------------------
|
| Here is where you can register web routes for your application. These
| routes are loaded by the RouteServiceProvider within a group which
| contains the "web" middleware group. Now create something great!
|
*/

Route::name('admin.')->namespace('Admin')->group(function () {
    Route::middleware(['guest:admin'])->group(function () {
        Route::get('/login', [LoginController::class, 'index'])->name('login.index');
        Route::post('/login', [LoginController::class, 'store'])->name('login.store');
    });
    Route::group(['middleware' => ['auth:admin']], function () {
        Route::get('/home', [HomeController::class, 'index'])->name('home.index');
        Route::get('/logout', [LoginController::class, 'logout'])->name('logout');
    });
});

Middleware auth:admin các trang yêu cầu auth với guard admin là người dùng thuộc bảng admins. Middleware auth là guard mặc định tương ứng với auth:web các trang thuộc middleware này cần auth với bảng users.

Middleware guest người dùng không cần auth có thể truy cập vào các trang này, người dùng đã đăng nhập truy cập vào các trang thuộc guest tự điều hướng về trang home.


<?php

namespace App\Http\Middleware;

use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

class RedirectIfAuthenticated
{
    /**
     * Handle an incoming request.
     *
     * @param  \Closure(\Illuminate\Http\Request): (\Symfony\Component\HttpFoundation\Response)  $next
     */
    public function handle(Request $request, Closure $next, string ...$guards): Response
    {
        $guards = empty($guards) ? [null] : $guards;

        foreach ($guards as $guard) {
            if ($guard == "admin" && Auth::guard($guard)->check()) {
                return redirect(RouteServiceProvider::ADMIN_HOME);
            }

            if (Auth::guard($guard)->check()) {
                return redirect(RouteServiceProvider::HOME);
            }
        }

        return $next($request);
    }
}

Middleware auth chỉ user đăng nhập mới vào được các trang này, nếu truy cập vào trang mà không có auth thì tự redirect ra trang login.


<?php

namespace App\Http\Middleware;

use Illuminate\Auth\Middleware\Authenticate as Middleware;
use Illuminate\Http\Request;

class Authenticate extends Middleware
{
    /**
     * Get the path the user should be redirected to when they are not authenticated.
     */
    protected function redirectTo(Request $request): ?string
    {
        if ($request->expectsJson()) {
            return null;
        }

        if ($request->is('admin') || $request->is('admin/*')) {
            return route('admin.login.index');
        }
        
        return route('user.login.index');
    }
}

Create controller

Tạo controller cho trang login, logout của user.


<?php

namespace App\Http\Controllers;

use Inertia\Inertia;
use Illuminate\Support\Facades\Auth;
use App\Http\Requests\AuthRequest;

class LoginController extends Controller
{
    public function index()
    {
        return Inertia::render('Auth/Login');
    }

    public function store(AuthRequest $request)
    {
        $auth = Auth::attempt([
            'email' => $request->email,
            'password' => $request->password,
        ],
            $request->get('remember')
        );

        if (!$auth) {
            return back()->withErrors([
                'email' => 'The provided credentials do not match our records.',
            ])->onlyInput('email');
        }

        return redirect()->route('user.home.index');
    }

    public function logout()
    {
        Auth::logout();

        return redirect('/login');
    }
}

Tạo AuthRequest validate input khi người dùng đăng nhập.


<?php

namespace App\Http\Requests;

use Illuminate\Foundation\Http\FormRequest;

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|string>
     */
    public function rules(): array
    {
        return [
            'email' => 'required|email',
            'password' => 'required|string|min:6',
        ];
    }
}

Tạo controller cho trang home của user, trang yêu cầu người dùng đăng nhập với thông tin của bảng user mới có thể truy cập được.


<?php

namespace App\Http\Controllers\Admin;

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

class HomeController extends Controller
{
    public function index()
    {
        $admin = Auth::guard('admin')->user();

        return Inertia::render('Admin/Welcome', ['admin' => $admin]);
    }
}

Sử dụng các view mặc định khi cài đặt laravel jetstream inertia trong thư mục resources/js/Pages

View cho trang login cho user trong file resources/js/Pages/Auth/Login.vue


<script setup>
import { Head, Link, useForm } from '@inertiajs/vue3';
import AuthenticationCard from '@/Components/AuthenticationCard.vue';
import AuthenticationCardLogo from '@/Components/AuthenticationCardLogo.vue';
import Checkbox from '@/Components/Checkbox.vue';
import InputError from '@/Components/InputError.vue';
import InputLabel from '@/Components/InputLabel.vue';
import PrimaryButton from '@/Components/PrimaryButton.vue';
import TextInput from '@/Components/TextInput.vue';

defineProps({
    canResetPassword: Boolean,
    status: String,
});

const form = useForm({
    email: '',
    password: '',
    remember: false,
});

const submit = () => {
    form.transform(data => ({
        ...data,
        remember: form.remember ? 'on' : '',
    })).post(route('user.login.store'), {
        onFinish: () => form.reset('password'),
    });
};
</script>

<template>
    <Head title="Log in" />

    <AuthenticationCard>
        <template #logo>
            <AuthenticationCardLogo />
        </template>

        <div v-if="status" class="mb-4 font-medium text-sm text-green-600">
            {{ status }}
        </div>

        <form @submit.prevent="submit">
            <div>
                <InputLabel for="email" value="Email" />
                <TextInput
                    id="email"
                    v-model="form.email"
                    type="email"
                    class="mt-1 block w-full"
                    required
                    autofocus
                    autocomplete="username"
                />
                <InputError class="mt-2" :message="form.errors.email" />
            </div>

            <div class="mt-4">
                <InputLabel for="password" value="Password" />
                <TextInput
                    id="password"
                    v-model="form.password"
                    type="password"
                    class="mt-1 block w-full"
                    required
                    autocomplete="current-password"
                />
                <InputError class="mt-2" :message="form.errors.password" />
            </div>

            <div class="block mt-4">
                <label class="flex items-center">
                    <Checkbox v-model:checked="form.remember" name="remember" />
                    <span class="ml-2 text-sm text-gray-600">Remember me</span>
                </label>
            </div>

            <div class="flex items-center justify-end mt-4">
                <Link v-if="canResetPassword" :href="route('password.request')" class="underline text-sm text-gray-600 hover:text-gray-900 rounded-md focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500">
                    Forgot your password?
                </Link>

                <PrimaryButton class="ml-4" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
                    Log in
                </PrimaryButton>
            </div>
        </form>
    </AuthenticationCard>
</template>

Tạo controller cho trang login, logout admin


<?php

namespace App\Http\Controllers\Admin;

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

class LoginController extends Controller
{
    public function index()
    {
        return Inertia::render('Admin/Auth/Login');
    }

    public function store(AuthRequest $request)
    {
        $auth = Auth::guard('admin')
            ->attempt([
                'email' => $request->email,
                'password' => $request->password,
            ],
                $request->get('remember')
            );

        if (!$auth) {
            return back()->withErrors([
                'email' => 'The provided credentials do not match our records.',
            ])->onlyInput('email');
        }

        return redirect()->route('admin.home.index');
    }

    public function logout(Request $request)
    {
        Auth::guard('admin')->logout();
        $request->session()->invalidate();
        $request->session()->regenerateToken();

        return redirect('/admin/login');
    }
}

Tạo controller cho trang home của admin


<?php

namespace App\Http\Controllers\Admin;

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

class HomeController extends Controller
{
    public function index()
    {
        $admin = Auth::guard('admin')->user();

        return Inertia::render('Admin/Welcome', ['admin' => $admin]);
    }
}

Các view của admin tạo tương tự như view của user.

Run application

Khi làm hết các bước trên là chúng ta đã có thể sử dụng multi auth rồi, giờ chỉ việc chạy ứng dụng và login vào trang user hoặc admin thôi. Để chạy ứng dụng có thể dùng docker, hoặc command sau


php artisan serve

Đăng nhập vào các trang dành cho user qua link http://localhost:8000/login. Sau khi đăng nhập xong người dùng sẽ được chuyển hướng về trang http://localhost:8000/home. Những người dùng chưa đăng nhập truy cập vào trang home sẽ chuyển hướng về trang login.

Đăng nhập vào các trang dành cho admin qua link http://localhost:8000/admin/login. Khi user đăng nhập xong thì sẽ được chuyển hướng về trang http://localhost:8000/admin/home. User chưa đăng nhập sẽ chuyển về trang login của admin

Các user đăng nhập qua bảng user không có quyền truy cập vào các trang admin và các user đăng nhập qua bẳng admin cũng không có quyền truy cập các trang của user

Sử dụng auth multi với guard cũng có thể phân quyền theo từng guard khi sử dụng laravel permission giúp chúng ta có thể phân quyên cho 2 site user và admin độc lập

Tổng kết

Như vậy là chúng ta đã sử dụng được multi auth với laravel 10 sử dụng laravel jetstream inertia, sử dụng view blade thông thường cũng làm tương tự như trên chỉ khác nhau ở view. Thanks for reading...