## **简介**
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`,就会跳转到后台登录页面。