Phần lớn các hệ thống website thông thường sẽ gồm phần quản lý admin và phần user thông thường sử dụng. Chính vì vậy các web site sẽ rất cần sử dụng multiple authentication. Cùng mình xây dựng chức năng này nhá!
Contents
Chuẩn bị
Các vấn đề gặp phải khi không sử dụng “multiple authentication”
Lúc trước khi chân ướt chân ráo, mình sử dụng bảng users
xác thực cho cả admin và người dùng. Mình sử dụng 1 field is_admin
(boolean) để kiểm tra nó là admin hay là user. Và mình dùng middleware để chặn người dùng truy cập vào trang admin. Sau đó nó nảy sinh khác nhiều vấn đề như sau:
- Khi admin đã xác thực thì có thể truy cập vào các quyền của user. Cực kỳ rối.
- 1 người vừa là admin vừa là user nhưng chỉ có 1 email thì không tạo được.
- Code không cẩn thận rất dễ thay đổi quyền user thành admin khi cập nhật thông tin.
- Dễ xảy ra lỗi dữ liệu (không chính xác) cho thống kê.
- Và một số vấn đề khác mà khi làm việc với nó bạn sẽ thấy.
Ta bắt tay vào xây dựng thôi nào.
Lưu ý: Trước khi xem bài này các bạn phải xem qua bài Authentication trong Laravel và thực hiện theo để có các file cần thiết cho phần user và hiểu được những gì bên dưới này nhá
Cấu trúc bảng
Ở đây mình sẽ xây dùng 2 luồng cơ bản: Là admin
và user
.
Phần user đã có sẵn rồi nên ta chỉ làm cho thằng admin thôi, hehe.
Admin Tạo bảng cần thiết cho admin
php artisan make:migration create_admins_table --create=admins
php artisan make:migration create_admin_password_resets_table --create=admin_password_resets
Thêm nội dung vào file vừa tạo
class CreateAdminsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('admins', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('email')->unique();
$table->string('password');
$table->rememberToken();
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('admins');
}
}
class CreateAdminPasswordResetsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('admin_password_resets', function (Blueprint $table) {
$table->string('email')->index();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('admin_password_resets');
}
}
Sau khi thay đổi nhớ chạy migrate
để tạo các bảng:
php artisan migrate
Models
Khai báo model Admin tương tự như thằng User
Sử dụng command:
php artisan make:model Admin
Mở model Admin
vừa tạo và cập nhật code bên trong đó:
namespace App\Models;
use Illuminate\Notifications\Notifiable;
use Illuminate\Foundation\Auth\User as Authenticatable;
class Admin extends Authenticatable
{
use Notifiable;
protected $fillable = [
'name',
'email',
'password',
];
protected $hidden = [
'password',
'remember_token',
];
}
Config
Những cái mình làm ở đây tương tự như thằng User, tính ra là còn ít hơn nữa là. Chẳng qua thằng User được Laravel setup sẵn nên mình thấy nhanh, còn thằng Admin phải tạo bằng tay thoi.
Mở file config/auth.php
và cấu hình guards
và provider
(chèn vào chứ không phải thêm mới nhang);
'guards' => [
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
],
'providers' => [
'admins' => [
'driver' => 'eloquent',
'model' => App\Models\Admin::class,
],
],
'passwords' => [
'admins' => [
'provider' => 'admins',
'table' => 'admin_password_resets',
'expire' => 60,
'throttle' => 60,
],
],
Nhớ rằng phải cấu hình file .env
để có thể gửi mail được nha. Xem thêm tại đây
Multiple authenticate
Vậy là xong phần chuẩn bị rồi. Bây giờ chúng ta bắt tay vào thực hiện làm nhiều luồng xác thực cho trang web.
Ở phần trước (Xem tại đây), mình đã giới thiệu các chức năng của các controller, nên phần này mình sẽ nói nhanh.
Route
Khi cài đặt package laravel/ui
đã khởi tạo route
cần thiết cho chúng ta. nhiều khi các bạn không biết vì sau nó chạy nữa là. Mở file vendor/laravel/ui/src/AuthRouteMethod.php
và theo dõi các route bên dưới
<?php
namespace Laravel\Ui;
class AuthRouteMethods
{
/**
* Register the typical authentication routes for an application.
*
* @param array $options
* @return callable
*/
public function auth()
{
return function ($options = []) {
// Login Routes...
if ($options['login'] ?? true) {
$this->get('login', 'Auth\LoginController@showLoginForm')->name('login');
$this->post('login', 'Auth\LoginController@login');
}
// Logout Routes...
if ($options['logout'] ?? true) {
$this->post('logout', 'Auth\LoginController@logout')->name('logout');
}
// Registration Routes...
if ($options['register'] ?? true) {
$this->get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
$this->post('register', 'Auth\RegisterController@register');
}
// Password Reset Routes...
if ($options['reset'] ?? true) {
$this->resetPassword();
}
// Password Confirmation Routes...
if ($options['confirm'] ??
class_exists($this->prependGroupNamespace('Auth\ConfirmPasswordController'))) {
$this->confirmPassword();
}
// Email Verification Routes...
if ($options['verify'] ?? false) {
$this->emailVerification();
}
};
}
/**
* Register the typical reset password routes for an application.
*
* @return void
*/
public function resetPassword()
{
return function () {
$this->get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
$this->post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
$this->get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
$this->post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
};
}
/**
* Register the typical confirm password routes for an application.
*
* @return void
*/
public function confirmPassword()
{
return function () {
$this->get('password/confirm', 'Auth\ConfirmPasswordController@showConfirmForm')->name('password.confirm');
$this->post('password/confirm', 'Auth\ConfirmPasswordController@confirm');
};
}
/**
* Register the typical email verification routes for an application.
*
* @return void
*/
public function emailVerification()
{
return function () {
$this->get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
$this->get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify');
$this->post('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
};
}
}
Tùy vào $option
mà mình nên truyền gì vào trong Auth::routes(['verify' => true]);
như bài trước.
Mình sẽ tạo file route mới dành cho admin routes/admin.php
. Trong đó khai báo những route cần thiết:
<?php
use App\Http\Controllers\Admin\Auth\LoginController;
use App\Http\Controllers\Admin\Auth\ForgotPasswordController;
use App\Http\Controllers\Admin\Auth\ResetPasswordController;
use App\Http\Controllers\Admin\DashboardController;
Route::get('login', [LoginController::class, 'showLoginForm'])->name('admin.login');
Route::post('login', [LoginController::class, 'login']);
Route::post('logout', [LoginController::class, 'logout'])->name('admin.logout');
Route::get('logout', [LoginController::class, 'logout']); // @Todo Remove logout GET method
Route::get('password/reset', [ForgotPasswordController::class, 'showLinkRequestForm'])->name('admin.password.request');
Route::post('password/email', [ForgotPasswordController::class, 'sendResetLinkEmail'])->name('admin.password.email');
Route::get('password/reset/{token}', [ResetPasswordController::class, 'showResetForm'])->name('password.reset');
Route::post('password/reset', [ResetPasswordController::class, 'reset'])->name('admin.password.update');
Route::middleware('admin.auth')->group(function () {
Route::get('dashboard', [DashboardController::class, 'dashboard'])->name('admin.dashboard');
});
Tạo file như này vẫn chưa sử dụng được đâu nha. Phải khai báo nó vào app/Providers/RouteServiceProvider.php
public function boot()
{
$this->routes(function () {
Route::prefix('api')
->middleware('api')
->namespace($this->namespace)
->group(base_path('routes/api.php'));
Route::middleware('web')
->namespace($this->namespace)
->group(base_path('routes/web.php'));
Route::middleware('web')
->prefix('admin')
->namespace($this->namespace)
->group(base_path('routes/admin.php'));
});
}
Trên đây mình khai báo một nhóm route có prefix là admin
nằm trong file routes/admin.php
.
Controller
Nhìn vào phần khai báo các route, có sử dụng các controller. Tất nhiên mình cũng tạo các controller theo folder đó.
Lần lượt dùng lệnh command hoặc tạo file và copy code vào:
ForgotPasswordController
<?php
namespace App\Http\Controllers\Admin\Auth;
use Illuminate\Foundation\Auth\SendsPasswordResetEmails;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
class ForgotPasswordController extends Controller
{
use SendsPasswordResetEmails;
protected function guard()
{
return Auth::guard('admin');
}
protected function broker()
{
return Password::broker('admins');
}
public function showLinkRequestForm()
{
return view('admin.auth.passwords.email');
}
}
LoginController
<?php
namespace App\Http\Controllers\Admin\Auth;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use Illuminate\Http\Response;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
use AuthenticatesUsers;
public function __construct()
{
$this->middleware('admin.guest')->except('logout');
}
protected function guard()
{
return Auth::guard('admin');
}
public function showLoginForm()
{
return view('admin.auth.login');
}
protected function redirectTo()
{
return route('admin.dashboard');
}
protected function loggedOut(Request $request)
{
return $request->wantsJson()
? new Response('', 204)
: redirect()->route('admin.login');
}
}
ResetPasswordController
<?php
namespace App\Http\Controllers\Admin\Auth;
use Illuminate\Foundation\Auth\ResetsPasswords;
use Illuminate\Http\Request;
use Illuminate\Routing\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Password;
class ResetPasswordController extends Controller
{
use ResetsPasswords;
public function showResetForm(Request $request, $token = null)
{
return view('admin.auth.passwords.reset')->with(
['token' => $token, 'email' => $request->email]
);
}
public function broker()
{
return Password::broker('admins');
}
protected function guard()
{
return Auth::guard('admin');
}
protected function redirectTo()
{
return route('admin.dashboard');
}
}
DashboardController
<?php
namespace App\Http\Controllers\Admin;
use App\Http\Controllers\Controller;
use Illuminate\Http\Request;
class DashboardController extends Controller
{
public function __construct()
{
// đã khai báo middleware ở route group
}
public function dashboard()
{
return view('admin.dashboard');
}
}
Trong các function trên, nếu không hiểu chỗ nào cứ mạnh dạng để lại comment ở phía bình luận hoặc trao đổi trực tiếp với mình thông qua các kênh thông tin ở phần bên dưới nhá
Middleware
Xem xơ xơ qua các route và controller phía trên thì mình có sử dụng 2 middleware đó là admin.auth
và admin.guest
Tiến hành tạo 2 file middlware nhá:
php artisan make:middleware AdminAuth
php artisan make:middleware RedirectIfAdminAuth
2 file sau khi được tạo nằm trong app/Http/Middleware
. Nếu chưa thấy thì nhớ reload from disk
.
Chèn code vào nhang:
AdminAuth
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Contracts\Auth\Factory as Auth;
class AdminAuth
{
protected $auth;
protected $guard = 'admin';
public function __construct(Auth $auth)
{
$this->auth = $auth;
}
public function handle($request, Closure $next)
{
$this->authenticate($request);
return $next($request);
}
protected function authenticate($request)
{
if ($this->auth->guard($this->guard)->check()) {
$this->auth->shouldUse($this->guard);
return;
}
$this->unauthenticated($request);
}
protected function unauthenticated($request)
{
throw new AuthenticationException(
'Unauthenticated.', [$this->guard], $this->redirectTo($request)
);
}
protected function redirectTo($request)
{
if (!$request->expectsJson()) {
return route('admin.login');
}
}
}
RedirectIfAdminAuth
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Support\Facades\Auth;
class RedirectIfAdminAuth
{
public function handle($request, Closure $next, $guard = 'admin')
{
if (Auth::guard($guard)->check()) {
return redirect()->route('admin.dashboard');
}
return $next($request);
}
}
Cuối cùng khai báo 2 middleware này vào app/Http/Kernel.php
protected $routeMiddleware = [
...
'admin.auth' => \App\Http\Middleware\AdminAuth::class,
'admin.guest' => \App\Http\Middleware\RedirectIfAdminAuth::class,
];
View
Tạo folder resources/views/admin
, sau đó copy folder auth
có sắn vào và chỉ giữ lại file email.blade.php, reset.blade.php, login.blade.php
.
Copy folder layouts
bỏ vào thằng admin lun. Đồng thời tạo file dashboard.blade.php
trong thằng admin này.
Thật ra những file này là clone từ thằng
user
ra, thay đổi lại các đường dẫn trong đó. Ở đây thây đổi các cáiroute
để handle phần admin và đường dẫn của fileapp.blade.php
email.blade.php
@extends('admin.layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Reset Password') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
<form method="POST" action="{{ route('admin.password.email') }}">
@csrf
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Send Password Reset Link') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
reset.blade.php
@extends('admin.layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Reset Password') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.password.update') }}">
@csrf
<input type="hidden" name="token" value="{{ $token }}">
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ $email ?? old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Reset Password') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
login.blade.php
@extends('admin.layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Login') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.login') }}">
@csrf
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email" autofocus>
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="current-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<div class="col-md-6 offset-md-4">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="remember" id="remember" {{ old('remember') ? 'checked' : '' }}>
<label class="form-check-label" for="remember">
{{ __('Remember Me') }}
</label>
</div>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-8 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Login') }}
</button>
@if (Route::has('admin.password.request'))
<a class="btn btn-link" href="{{ route('admin.password.request') }}">
{{ __('Forgot Your Password?') }}
</a>
@endif
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
app.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">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- Left Side Of Navbar -->
<ul class="navbar-nav mr-auto">
</ul>
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Authentication Links -->
@if(Auth::guard('admin')->check())
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::guard('admin')->user()->name }}
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('admin.logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" class="d-none">
@csrf
</form>
</div>
</li>
@else
@if (Route::has('admin.login'))
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
</li>
@endif
@endif
</ul>
</div>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>
dashboard.blade.php
@extends('admin.layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Dashboard') }}</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
{{ __('You are logged in!') }}
</div>
</div>
</div>
</div>
</div>
@endsection
Đến đây là hoàn tất quá trình. Cuối cùng bạn phải tạo 1 tài khoản admin bằng tay vì admin không có chức năng đăng ký tài khoản. Sau đó thực hiện chức năng quên mật khẩu và khôi phục mật khẩu.
Tips
Vì admin không có chức năng đăng ký tài khoản, nên khi vừa khởi chạy project ta phải tạo 1 tài khoản admin, sau đó phải quên mật khẩu để lấy lại mật khẩu mới. Cực kì mất công nó làm cho hệ thống thiếu chuyên nghiệp. Thay vì đó mình sẽ tạo 1 lệnh command
và khi khởi chạy project của mình, mình chỉ cần chạy lệnh và nhập thông tin trực tiếp vào đó. Ví dụ như email, password.
Tham khảo Artisan Console trong Laravel
Kết luận
Bài viết trên mình đã hướng dẫn cho các bạn về multiple authentication
rất chi tiết.
Nếu gặp lỗi gì trong quá trình cài đặt hoặc kiến thức mình cung cấp không chính xác thì có thể liên hệ trực tiếp với minh qua các kênh thông tin phía cuối trang.
Rất mong được sự ủng hộ của mọi người để mình có động lực ra những bài viết tiếp theo.
{\__/}
( ~.~ )
/ > ♥️ I LOVE YOU 3000
JUST DO IT!