## **簡介**
Laravel 支持基于多表的用戶認證,即同時允許不同數據表用戶(如前臺用戶、后臺用戶)進行登錄認證。下面我們就以前后臺用戶登錄認證為例,簡單介紹基于不同數據表實現用戶注冊及登錄功能。
## **1. 實現前臺用戶登錄**
我們用框架自帶的`users`表存儲前臺用戶,接下來我們先實現前臺用戶注冊登錄。通過之前運行的`make:auth`命令,我們已經生成了前臺認證所需的所有代碼和數據表,并且我們在[上一篇教程](https://xueyuanjun.com/post/9734.html)中知曉了`users`表存儲用戶的注冊登錄流程。
## **2. 創建后臺用戶模型**
假設我們的后臺用戶表是`admins`,對應的模型類是`Admin`,首先使用如下 Artisan 命令生成后臺用戶模型及對應數據庫遷移文件:
~~~
php artisan make:model Admin -m
~~~
編輯新生成的數據庫遷移文件`create_admins_table`對應遷移類的`up`方法代碼如下(直接從`users`表遷移中拷貝過來):
~~~
<?php
namespace App;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Admin extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array
*/
protected $fillable = [
'name', 'email', 'password',
];
/**
* The attributes excluded from the model's JSON form.
*
* @var array
*/
protected $hidden = [
'password', 'remember_token',
];
}
~~~
然后運行遷移命令——`php artisan migrate`在數據庫中創建該表。
## **3. 編輯認證配置文件**
要實現多表用戶認證,首先要配置認證配置文件`auth.php`,這里我們實現的功能是前后臺用戶登錄,所以對應配置如下,該配置文件默認的用戶認證相關配置如下:
~~~
// 默認用戶認證配置,即不指定特定認證服務方的話,使用以下默認配置
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
// 在這里配置不同的認證服務方,默認支持 web 和 api 認證,
// 即 web 路由的請求認證和 api 路由的請求認證
// 如果要配置其它的認證服務方,比如后臺登錄,需要在這里配置
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
// 在這里配置系統支持的認證提供者(用戶數據來源),
// 默認是基于 User 模型的 EloquentProvider,
// 如果系統支持不同表用戶登錄,需要在這里額外配置
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
// 'users' => [
// 'driver' => 'database',
// 'table' => 'users',
// ],
],
// 密碼重置表,默認支持 users 表的密碼重置,對應數據表是 password_resets
// 如果要支持其它用戶表的密碼重置,需要在這里額外配置
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
],
~~~
我在注釋里指出了每個配置項的作用,以及如果要配置多表用戶認證,需要怎么配置。我們仍然使用默認的`web`guard 實現前臺登錄,接下來依樣畫葫蘆,新增一個`admin`guard 用于后臺登錄,然后在`providers`中新增一個后臺用戶數據提供者`admins`,后臺用戶都是自己人,就不做密碼重置了,如果你面對的用戶系統更復雜,比如電商系統,涉及買家、賣家、系統后臺用戶,則可能需要為不同用戶表設置密碼重置表,在`passwords`配置項中參照`users`表進行設置就好了,然后還要創建對應的數據表。下面是更新后的`auth.php`配置表:
~~~
<?php
return [
'defaults' => [
'guard' => 'web',
'passwords' => 'users',
],
'guards' => [
'web' => [
'driver' => 'session',
'provider' => 'users',
],
'admin' => [
'driver' => 'session',
'provider' => 'admins',
],
'api' => [
'driver' => 'token',
'provider' => 'users',
],
],
'providers' => [
'users' => [
'driver' => 'eloquent',
'model' => App\User::class,
],
'admins' => [
'driver' => 'eloquent',
'model' => App\Admin::class,
],
],
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_resets',
'expire' => 60,
],
],
];
~~~
認證配置是由`guard`和`provider`兩部分構成的,`guard`用于配置認證請求服務方,比如前臺登錄、后臺登錄、API登錄,以及基于 Web 路由還是 API 路由(Web 路由基于 Session 進行認證,API 路由基于 Token 進行認證);`provider`用于配置用戶認證數據提供方,通過 Eloquent 還是數據庫,以及哪張數據表。所以我們在這兩個配置項中分別新增了`admin`和`admins`配置項,標識后臺登錄基于 Web 路由,并且由`Admin`模型類提供數據支持。
## **4. 定義后臺用戶認證路由及控制器**
做好以上準備工作后,接下來我們正式開始實現后臺認證。首先定義后臺用戶認證路由,在`routes/web.php`中新增如下路由定義:
~~~
Route::get('admin/login', 'Admin\LoginController@showLoginForm')->name('admin.login');
Route::post('admin/login', 'Admin\LoginController@login');
Route::get('admin/register', 'Admin\RegisterController@showRegistrationForm')->name('admin.register');
Route::post('admin/register', 'Admin\RegisterController@register');
Route::post('admin/logout', 'Admin\LoginController@logout')->name('admin.logout');
Route::get('admin', 'AdminController@index')->name('admin.home');
~~~
然后使用Artisan命令創建對應控制器:
~~~
php artisan make:controller Admin/LoginController
php artisan make:controller Admin/RegisterController
php artisan make:controller AdminController
~~~
編輯`Admin/LoginController.php`代碼如下:
~~~
<?php
namespace App\Http\Controllers\Admin;
use Illuminate\Foundation\Auth\AuthenticatesUsers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
class LoginController extends Controller
{
use AuthenticatesUsers;
protected $redirectTo = '/admin';
public function __construct()
{
$this->middleware('guest:admin')->except('logout');
}
public function showLoginForm()
{
return view('admin.login');
}
protected function guard()
{
return Auth::guard('admin');
}
// 退出后跳轉頁面
protected function loggedOut(Request $request)
{
return redirect(route('admin.login'));
}
}
~~~
可以看到我們重寫了`AuthenticatesUsers`中的兩個方法,`showLoginForm()`方法用戶顯示后臺登錄表單,`guard()`方法用于在`Auth::guard`方法中傳入對應的認證服務方配置并將其返回,這樣,后臺登錄時使用的就是上一步配置的后臺認證 Guard 了,該參數值默認是`web`(在`auth.php`的`defaults`中配置),所以我們在前臺用戶認證時不需要手動傳入。
另外,我們在中間件`guest`中也傳入了`admin`參數,表示判斷后臺是否登錄。為此,我們還要修改`guest`中間件對應類`RedirectIfAuthenticated`的處理方法`handle`,當傳入`guard`是`admin`時,跳轉到后臺主頁:
~~~
public function handle($request, Closure $next, $guard = null)
{
if (Auth::guard($guard)->check()) {
if ($guard == 'admin') {
return redirect('/admin');
}
return redirect('/home');
}
return $next($request);
}
~~~
最后,我們還設置了登錄后跳轉路徑為`/admin`并重寫`loggedOut`方法返回退出后臺后調整頁面為后臺登錄頁面。
同理,編輯`Admin/RegisterController.php`代碼如下:
~~~
<?php
namespace App\Http\Controllers\Admin;
use App\Admin;
use Illuminate\Foundation\Auth\RegistersUsers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Validator;
class RegisterController extends Controller
{
use RegistersUsers;
protected $redirectTo = '/admin';
public function __construct()
{
$this->middleware('guest:admin');
}
public function showRegistrationForm()
{
return view('admin.register');
}
protected function guard()
{
return Auth::guard('admin');
}
/**
* Get a validator for an incoming registration request.
*
* @param array $data
* @return \Illuminate\Contracts\Validation\Validator
*/
protected function validator(array $data)
{
return Validator::make($data, [
'name' => 'required|string|max:255',
'email' => 'required|string|email|max:255|unique:admins',
'password' => 'required|string|min:6|confirmed',
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
protected function create(array $data)
{
return Admin::create([
'name' => $data['name'],
'email' => $data['email'],
'password' => Hash::make($data['password']),
]);
}
}
~~~
相應的邏輯和登錄類似,就不多說了。然后編輯`AdminController.php`代碼如下:
~~~
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
class AdminController extends Controller
{
/**
* Create a new controller instance.
*
* @return void
*/
public function __construct()
{
$this->middleware('auth:admin');
}
/**
* Show the application dashboard.
*
* @return \Illuminate\Http\Response
*/
public function index()
{
return view('admin.home');
}
}
~~~
通過`index`方法渲染后臺登錄后頁面。如果沒有登錄訪問該方法會通過`auth`中間件進行處理,我們在該中間件中也傳入了`admin`參數,用于判斷后臺是否登錄,同樣,我們也要修改`auth`中間件對應類`Authenticate`來處理后臺未登錄跳轉,這一次我們通過重寫父類的`authenticate`方法來實現:
~~~
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
protected $redirectTo = '';
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string
*/
protected function redirectTo($request)
{
return route('login');
}
protected function authenticate($request, array $guards)
{
if (empty($guards)) {
$guards = [null];
}
foreach ($guards as $guard) {
if ($this->auth->guard($guard)->check()) {
return $this->auth->shouldUse($guard);
}
}
// 這里我們以 guards 傳入的第一個參數為準選擇跳轉到的登錄頁面
$guard = $guards[0];
if ($guard == 'admin') {
$this->redirectTo = route('admin.login');
}
throw new AuthenticationException(
'Unauthenticated.', $guards, $this->redirectTo ? : $this->redirectTo($request)
);
}
}
~~~
## **視圖文件創建及修改**
在創建后臺認證視圖之前,先要為后臺認證創建一個布局文件,我們拷貝`layouts/app.blade.php`進行編寫
~~~
cp resources/views/layouts/app.blade.php resources/views/layouts/admin.blade.php
~~~
然后修改`admin.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') }} Admin</title>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet" type="text/css">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light navbar-laravel">
<div class="container">
<a class="navbar-brand" href="{{ url('/admin') }}">
{{ config('app.name', 'Laravel') }} Admin
</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">
<li class="nav-item">
<a class="nav-link" href="{{ url('/') }}">Home</a>
</li>
</ul>
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Authentication Links -->
@guest
<li class="nav-item">
<a class="nav-link" href="{{ route('admin.login') }}">{{ __('Login') }}</a>
</li>
<li class="nav-item">
@if (Route::has('admin.register'))
<a class="nav-link" href="{{ route('admin.register') }}">{{ __('Register') }}</a>
@endif
</li>
@else
<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::user('admin')->name }} <span class="caret"></span>
</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('admin.logout') }}" method="POST" style="display: none;">
@csrf
</form>
</div>
</li>
@endguest
</ul>
</div>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>
~~~
可以看到我們將其中的注冊、登錄、退出鏈接全部替換成了后臺認證相關的鏈接,修改了導航欄,新增了「Home」鏈接跳轉到應用首頁,并且在用戶登錄后通過`Auth::user('admin')->name`獲取后臺登錄用戶名,這里的邏輯和前面后臺認證控制器一樣,通過在`Auth::user()`方法中傳入指定的用戶認證服務方來獲取對應的用戶認證信息,默認值是`web`,所以我們在前臺用戶登錄流程中不需要手動傳入這個參數值。
最后我們創建后臺用戶認證對應視圖文件,先在`resources/views`目錄下創建`admin`子目錄,然后將前臺用戶認證視圖模板拷貝過去并稍作修改即可:
~~~
cp resources/views/auth/login.blade.php resources/views/admin/
cp resources/views/auth/register.blade.php resources/views/admin/
cp resources/views/home.blade.php resources/views/admin/
~~~
修改`admin/login.blade.php`代碼如下:
~~~
@extends('layouts.admin')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Admin Login') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.login') }}">
@csrf
<div class="form-group row">
<label for="email" class="col-sm-4 col-form-label text-md-right">{{ __('Email') }}</label>
<div class="col-md-6">
<input id="email" class="form-control{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required autofocus>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</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{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
@if ($errors->has('password'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</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>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
~~~
修改`admin/register.blade.php`代碼如下:
~~~
@extends('layouts.admin')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">{{ __('Admin Register') }}</div>
<div class="card-body">
<form method="POST" action="{{ route('admin.register') }}">
@csrf
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control{{ $errors->has('name') ? ' is-invalid' : '' }}" name="name" value="{{ old('name') }}" required autofocus>
@if ($errors->has('name'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('name') }}</strong>
</span>
@endif
</div>
</div>
<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{{ $errors->has('email') ? ' is-invalid' : '' }}" name="email" value="{{ old('email') }}" required>
@if ($errors->has('email'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('email') }}</strong>
</span>
@endif
</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{{ $errors->has('password') ? ' is-invalid' : '' }}" name="password" required>
@if ($errors->has('password'))
<span class="invalid-feedback" role="alert">
<strong>{{ $errors->first('password') }}</strong>
</span>
@endif
</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>
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Register') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
~~~
修改`admin/home.blade.php`代碼如下:
~~~
@extends('layouts.admin')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Admin Dashboard</div>
<div class="card-body">
@if (session('status'))
<div class="alert alert-success" role="alert">
{{ session('status') }}
</div>
@endif
You are logged in the admin dashboard!
</div>
</div>
</div>
</div>
</div>
@endsection
~~~
至此,后臺用戶注冊登錄的所有功能都實現了,在瀏覽器中訪問`http://blog.test/admin`,就會跳轉到后臺登錄頁面。