基于多表的用户认证功能实现(前后台用户)


Laravel 支持基于多表的用户认证,即同时允许不同数据表用户(如前台用户、后台用户)进行登录认证。下面我们就以前后台用户登录认证为例,简单介绍基于不同数据表实现用户注册及登录功能。

1、生成认证脚手架

关于这一块内容我们已经在通过内置脚手架快速实现用户认证这篇教程中详细讨论过,如果你对此不了解,可以去查看对应内容,如果已经看过,请跳过此步骤。

2、实现前台用户登录

我们用框架自带的 users 表存储前台用户,接下来我们先实现前台用户注册登录。通过之前运行的 make:auth 命令,我们已经生成了前台认证所需的所有代码和数据表,并且我们在上一篇教程中演示了 users 表存储用户的注册登录流程。

3、创建后台用户模型

我们重点介绍后台用户注册登录。假设我们的后台用户表是 admins,对应的模型类是 Admin,首先使用如下 Artisan 命令生成后台用户模型及对应数据库迁移文件:

php artisan make:model Admin -m

编辑新生成的数据库迁移文件 create_admins_table 对应迁移类的 up 方法代码如下(直接从 users 表迁移中拷贝过来):

public function up()
{
    Schema::create('admins', function (Blueprint $table) {
        $table->increments('id');
        $table->string('name');
        $table->string('email')->unique();
        $table->timestamp('email_verified_at')->nullable();
        $table->string('password');
        $table->rememberToken();
        $table->timestamps();
    });
}

然后运行迁移命令在数据库中创建该表:

php artisan migrate

然后更新 Admin 模型类如下:

<?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',
    ];
}

4、编辑认证配置文件

要实现多表用户认证,首先要配置认证配置文件 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,
        ],
    ],
];

认证配置是由 guardprovider 两部分构成的,guard 用于配置认证请求服务方,比如前台登录、后台登录、API登录,以及基于 Web 路由还是 API 路由(Web 路由基于 Session 进行认证,API 路由基于 Token 进行认证);provider 用于配置用户认证数据提供方,通过 Eloquent 还是数据库,以及哪张数据表。所以我们在这两个配置项中分别新增了adminadmins 配置项,标识后台登录基于 Web 路由,并且由 Admin 模型类提供数据支持。

5、定义后台用户认证路由及控制器

后台认证路由

做好以上准备工作后,接下来我们正式开始实现后台认证。首先定义后台用户认证路由,在 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.phpdefaults 中配置),所以我们在前台用户认证时不需要手动传入。

另外,我们在中间件 guest 中也传入了 admin 参数,表示判断后台是否登录。为此,我们还要修改 guest 中间件对应类 RedirectIfAuthenticated 的处理方法 handle,当传入 guardadmin 时,跳转到后台主页:

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)
        );
    }
}

6、视图文件创建及修改

创建后台布局文件

在创建后台认证视图之前,先要为后台认证创建一个布局文件,我们拷贝 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

7、后台用户认证测试

至此,后台用户注册登录的所有功能都实现了,在浏览器中访问 http://blog.test/admin,就会跳转到后台登录页面:

如果没有注册的话,点击右上角「Register」链接跳转到注册页面进行注册:

注册一个用户名为「管理员」的用户,注册成功后,页面跳转到 http://blog.test/admin,说明认证成功:

点击右上角隐藏的「Logout」链接,会跳转到后台登录页面。这样,一个完整的后台注册、登录、退出功能就完成了。


Vote Vote Cancel Collect Collect Cancel

<< 上一篇: 用户注册登录流程及多字段登录实现

>> 下一篇: 通过 Passport 实现 API 请求认证:单页面应用篇