如何在一个 Laravel 应用中使用多个认证看守器(Guards)

Laravel

如果你使用 Laravel 有一段时间了,你应该听过很多关于多种身份认证的信息。你也应该听过很多认证看守器。但是如果你对 Laravel 还不熟悉,多种身份认证可以让不同类别的用户访问相同应用的不同/类似的部分。

你可能希望在你的 Laravel 应用程序中使用多种身份认证的原因有很多。例如,你有一个运行整个公司的大型应用程序。客户还通过相同的应用程序与公司的产品和服务进行交互。该应用程序应该还有一个博客,公司中还有一个部门专门管理这个博客。

从上面的程序可以看出,这里面已经有三组用户。客户,我们可以让他们使用一个特定的身份验证过程来访问应用;作者,他们应该有另一个身份验证过程,甚至有权限来启用一个更健壮的内容管理流程;公司的其他成员,您应该根据他们不同的角色来给他们展示不同的功能菜单。
现在,让我们看看如何为不同类别的用户创建多个身份验证。

要求

  1. 知识储备 PHP (版本 >= 7.1.3)。
  2. 知识储备 Laravel (版本 5.6.x)。
  3. 电脑已安装Composer  (版本 >= 1.3.2)。
  4. 电脑已安装 Laravel 框架 。

开始

如果你已经满足上面清单上的所有条件,那么你可以继续本教程, 我们将创建拥有三个用户类的应用程序 — adminwriteruser。我们将为这三个用户类建立不同的 guards限制。

创建应用

我们创建一个新的Laravel应用程序,在终端执行下面的命令可以创建一个新的Laravel应用程序。

    $ laravel new multi-auth
    $ cd multi-auth

创建数据库

我们在应用程序中使用SQLite数据库。 它是一个轻量级并且非常快的文件类型数据库。 我们可以在命令行中创建数据库:

    $ touch database/database.sqlite

在你的应用中,打开 .env 文件并把下面的配置:

    DB_CONNECTION=mysql
    DB_HOST=127.0.0.1
    DB_PORT=3306
    DB_DATABASE=homestead
    DB_USERNAME=homestead
    DB_PASSWORD=secret

修改为:

    DB_CONNECTION=/absolute/path/to/database.sqlite

这将会确保让我们的应用使用SQLite数据库驱动程序。

创建数据库迁移

我们将会为adminswriters 表创建数据表迁移文件,类似users表的迁移文件。这都是简单的用户表,将来你可以根据你的需求来扩展他们。

创建admins数据表迁移文件

运行下面的命令创建admins数据表迁移文件:

    $ php artisan make:migration create_admins_table

在 database/migrations 目录, 打开并修改admins数据表迁移文件:

    // database/migrations/<timestamp>_create_admins_table.php

    [...]
    public function up()
    {
        Schema::create('admins', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->boolean('is_super')->default(false);
            $table->rememberToken();
            $table->timestamps();
        });
    }
    [...]

我们创建了一个简单的数据表迁移文件,并定义了我们要使用的数据表的字段。Eloquent为我们提供了方法来定义数据库表字段的数据类型。我们使用它们定义表字段的数据类型。

记住,你可以按你的想法配置你的表。

Create migration for writers

通过以下命令来创建 writers 的数据迁移表:

    $ php artisan make:migration create_writers_table

打开刚才创建的迁移表文件进行修改:

    database/migrations/<timestamp>_create_writers_table.php
    [...]
    public function up()
    {
        Schema::create('writers', function (Blueprint $table) {
            $table->increments('id');
            $table->string('name');
            $table->string('email')->unique();
            $table->string('password');
            $table->boolean('is_editor')->default(false);
            $table->rememberToken();
            $table->timestamps();
        });
    }
    [...]

我们仅仅创建了一个简单的迁移文件并定义了几个需要的字段。Eloquent 提供了很多定义数据库表字段类型的方法,所以操作起来比较容易。

迁移数据库

现在我们已经定义了表,我们开始迁移数据库:

    $ php artisan migrate

设置模型

我们的应用程序有不同类别的用户,他们存储在不同的数据库表中,要为使用这些不同的表的用户进行身份验证,我们必须为它们定义模型。这些模型类似于用户模型并扩展可验证类。

Admin 模型

执行下面的命令,创建 Admin 模型:

    $ php artisan make:model Admin

打开 Admin 模型文件 app/Admin.php,添加如下代码:

    // app/Admin.php
    <?php

    namespace App;

    use Illuminate\Notifications\Notifiable;
    use Illuminate\Foundation\Auth\User as Authenticatable;

    class Admin extends Authenticatable
    {
        use Notifiable;

        protected $guard = 'admin';

        protected $fillable = [
            'name', 'email', 'password',
        ];

        protected $hidden = [
            'password', 'remember_token',
        ];
    }

当你要对一个模型的访问做用户认证,但又不想用默认的 user 看守器时,就需要指定看守器。在这个例子中,这个看守器就是 admin

fillable 数组中,指定了用户可以操作的数据库字段。也就是明确地告知 Laravel:

当我执行 createupdate 方法时,我会把要操作的字段以数组的形式传给你,但是你只可以往数据库中插入 fillable 数组中指定的字段。

这样,就可以防止用户任意操作我们不希望被更改的字段。

hidden 数组中,可以指定不希望被返回的字段。

Writers 模型

开始为 Writer 创建模型,我们运行下面的命令:

    $ php artisan make:model Writer

接着我们打开 Writer 模型,用下面的代码替换掉:

    // app/Writer.php
    <?php

    namespace App;

    use Illuminate\Notifications\Notifiable;
    use Illuminate\Foundation\Auth\User as Authenticatable;

    class Writer extends Authenticatable
    {
        use Notifiable;

        protected $guard = 'writer';

        protected $fillable = [
            'name', 'email', 'password',
        ];

        protected $hidden = [
            'password', 'remember_token',
        ];
    }

定义守卫(Guards)

Laravel 的守卫定义了如何为每个请求进行身份验证。默认有一些用于身份验证的守卫,但也可以自定义。 这样可以使用 Laravel 默认的认证系统和自定义的 AdminWriter 模型。

打开 config/auth.php 并添加新的守卫,如下:

    // config/auth.php

    <?php

    [...]
    'guards' => [
        [...]
        'admin' => [
            'driver' => 'session',
            'provider' => 'admins',
        ],
        'writer' => [
            'driver' => 'session',
            'provider' => 'writers',
        ],
    ],
    [...]

添加了两个新的守卫 adminwriter,并设置了提供者。通过提供者来进行身份验证。

现在,完善 providers 数组:

    // config/auth.php

    [...]
    'providers' => [
        [...]
        'admins' => [
            'driver' => 'eloquent',
            'model' => App\Admin::class,
        ],
        'writers' => [
            'driver' => 'eloquent',
            'model' => App\Writer::class,
        ],
    ],
    [...]

根据上一步定义的看守器,我们具体完善了 providers 数组的信息。用 eloquent 作为驱动,是因为我们要用 Eloquent ORM 与数据库交互。

假设我们要用别的 ORM 比如 RedBeanPHP 来操作数据库,那就要把驱动设置为 redbeanphp,而不是 eloquent。 至于 model ,就是具体要进行访问验证的模型类。

设置控制器

为了使这些看守器发挥各自的作用,我们有两种方案可以选择,一是修改现有的验证控制器,二是创建新的控制器。你可以根据具体需要进行选择。在这个例子中,我们选择前者。

修改 LoginController

打开 LoginController,并做如下编辑:

    // app/Http/Controllers/Auth/LoginController.php

    <?php

    namespace App\Http\Controllers\Auth;

    use App\Http\Controllers\Controller;
    use Illuminate\Foundation\Auth\AuthenticatesUsers;
    [...]
    use Illuminate\Http\Request;
    use Auth;
    [...]
    class LoginController extends Controller
    {
        [...]
        public function __construct()
        {
            $this->middleware('guest')->except('logout');
            $this->middleware('guest:admin')->except('logout');
            $this->middleware('guest:writer')->except('logout');
        }
        [...]
    }

通过中间件的方式限制访问控制器里的方法。我们细分了访客的类型,这样,以一个类型的用户登录之后,如果再想切换身份,以另一种类型的用户登录时,就会被引导到预定义的身份验证页面。

举个例子: 如果我在电脑上以管理员的身份登录了,我的作家同事就无法以作者的身份登录他的账户。

这个验证操作是很重要的,这样就不会因为 session 信息的混乱,而给应用数据带来潜在的风险。

现在定义 admins 登陆:

    // app/Http/Controllers/Auth/LoginController.php

    [...]
    public function showAdminLoginForm()
    {
        return view('auth.login', ['url' => 'admin']);
    }

    public function adminLogin(Request $request)
    {
        $this->validate($request, [
            'email'   => 'required|email',
            'password' => 'required|min:6'
        ]);

        if (Auth::guard('admin')->attempt(['email' => $request->email, 'password' => $request->password], $request->get('remember'))) {

            return redirect()->intended('/admin');
        }
        return back()->withInput($request->only('email', 'remember'));
    }
    [...]

我们已经设置了一个方法来返回管理员的登录页面。 我们将对所有用户类型使用相同的页面,并仅更改它们发送到的 URL 。 为我们节省了许多我们可以避免编写的代码。

我们还定义了 adminLogin 方法,该方法检查是否提供了正确的凭据。 然后我们尝试使用 admin guard 登录用户。 在尝试登录时设置此守卫非常重要,以便Auth Facade将检查正确的表匹配凭据。 它还将设置我们的身份验证,以便我们可以根据登录用户的类型限制页面。
我们将经过身份验证的用户重定向到特定 URL,并将未经身份验证的用户返回登录页面。

现在,让我们为 writers 也做同样的事情:

    // app/Http/Controllers/Auth/LoginController.php

    [...]
    public function showWriterLoginForm()
    {
        return view('auth.login', ['url' => 'writer']);
    }

    public function writerLogin(Request $request)
    {
        $this->validate($request, [
            'email'   => 'required|email',
            'password' => 'required|min:6'
        ]);

        if (Auth::guard('writer')->attempt(['email' => $request->email, 'password' => $request->password], $request->get('remember'))) {

            return redirect()->intended('/writer');
        }
        return back()->withInput($request->only('email', 'remember'));
    }
    [...]

我们的登录设置好了。万岁!!!

修改 RegisterController

打开 RegisterController 并进行如下编辑:

    // app/Http/Controllers/Auth/RegisterController.php

    <?php
    [...]
    namespace App\Http\Controllers\Auth;
    use App\User;
    use App\Admin;
    use App\Writer;
    use App\Http\Controllers\Controller;
    use Illuminate\Support\Facades\Hash;
    use Illuminate\Support\Facades\Validator;
    use Illuminate\Foundation\Auth\RegistersUsers;
    use Illuminate\Http\Request;
    [...]
    class RegisterController extends Controller
    {
        [...]
        public function __construct()
        {
            $this->middleware('guest');
            $this->middleware('guest:admin');
            $this->middleware('guest:writer');
        }
      [...]
    }

我们已经设置了中间件,控制器将来会使用的到,就像我们使用 LoginController一样。

现在,让我们设置为不同用户返回不同的注册页面的方法:

    // app/Http/Controllers/Auth/RegisterController.php

    [...]
    public function showAdminRegisterForm()
    {
        return view('auth.register', ['url' => 'admin']);
    }

    public function showWriterRegisterForm()
    {
        return view('auth.register', ['url' => 'writer']);
    }
    [...]

这跟我们在显示不同登录页面时所做的类似。

现在,我们可以定义一个创建 admin 的方法:

    // app/Http/Controllers/Auth/RegisterController.php

    [...] 
    protected function createAdmin(Request $request)
    {
        $this->validator($request->all())->validate();
        $admin = Admin::create([
            'name' => $request['name'],
            'email' => $request['email'],
            'password' => Hash::make($request['password']),
        ]);
        return redirect()->intended('login/admin');
    }
    [...] 

接下来,让我们定义创建 writer 的方法:

    // app/Http/Controllers/Auth/RegisterController.php

    [...] 
    protected function createWriter(Request $request)
    {
        $this->validator($request->all())->validate();
        $writer = Writer::create([
            'name' => $request['name'],
            'email' => $request['email'],
            'password' => Hash::make($request['password']),
        ]);
        return redirect()->intended('login/writer');
    }
    [...] 

注册完成。

设置验证页面

我们将用 Laravel 自带的脚手架为验证系统生成页面和控制器。执行如下命令:

    $ php artisan make:auth

这样就可以生成模版文件 resources/views/auth, 同时还会生成相应的路由,从而完成基本的验证操作。很酷对不对?

打开 login.blade.php 文件并做如下编辑:

    // resources/views/auth/login.blade.php
    [...]
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header"> {{ isset($url) ? ucwords($url) : ""}} {{ __('Login') }}</div>

                    <div class="card-body">
                        @isset($url)
                        <form method="POST" action='{{ url("login/$url") }}' aria-label="{{ __('Login') }}">
                        @else
                        <form method="POST" action="{{ route('login') }}" aria-label="{{ __('Login') }}">
                        @endisset
                            @csrf
        [...]
    </div>

这里我们会判断是否有 url 参数。如果有,就把这个参数带到表单提交的路由里。同时也会修改表单页的头部信息,从而实现根据不同的登录参数展示用户的类型。

打开 register.blade.php 模板文件并编辑,如下:

    // resources/views/auth/register.blade.php

    [...]
    <div class="container">
        <div class="row justify-content-center">
            <div class="col-md-8">
                <div class="card">
                    <div class="card-header"> {{ isset($url) ? ucwords($url) : ""}} {{ __('Register') }}</div>

                    <div class="card-body">
                        @isset($url)
                        <form method="POST" action='{{ url("register/$url") }}' aria-label="{{ __('Register') }}">
                        @else
                        <form method="POST" action="{{ route('register') }}" aria-label="{{ __('Register') }}">
                        @endisset
                            @csrf
        [...]
    </div>

我们复制了登录页面的操作。

创建经过身份验证的用户将访问的页面

现在,我们已经完成了登录和注册页面的设置,让我们让 admin 和 writers 在验证页面时看到这些页面。打开终端并运行以下命令来创建新文件。接下来,我们将把相应的代码片段插入到文件中。

    $ touch resources/views/layouts/auth.blade.php
    $ touch resources/views/admin.blade.php
    $ touch resources/views/writer.blade.php
    $ touch resources/views/home.blade.php

将此代码块插入 auth.blade.php 文件:

    // resources/views/layouts/auth.blade.php

    <!DOCTYPE html>
    <html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
    <head>
        <meta charset="utf-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <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="https://fonts.gstatic.com">
        <link href="https://fonts.googleapis.com/css?family=Raleway:300,400,600" 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('/') }}">
                        {{ 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 -->
                           <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>
                                    Hi There <span class="caret"></span>
                                </a>

                                <div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
                                    <a class="dropdown-item" href="{{ route('logout') }}"
                                       onclick="event.preventDefault();
                                                     document.getElementById('logout-form').submit();">
                                        {{ __('Logout') }}
                                    </a>

                                    <form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
                                        @csrf
                                    </form>
                                </div>
                            </li>
                        </ul>
                    </div>
                </div>
            </nav>

            <main class="py-4">
                @yield('content')
            </main>
        </div>
    </body>
    </html>

接下来, 将此代码块插入 admin.blade.php 文件:

    // resources/views/admin.blade.php

    @extends('layouts.auth')

    @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">
                        Hi boss!
                    </div>
                </div>
            </div>
        </div>
    </div>
    @endsection

打开  writer.blade.php 模板文件并编辑,如下:

    // resources/views/writer.blade.php

    @extends('layouts.auth')

    @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">
                        Hi there, awesome writer
                    </div>
                </div>
            </div>
        </div>
    </div>
    @endsection

最后, 打开  home.blade.php  模板文件并替换为:

    // resources/views/home.blade.php

    @extends('layouts.auth')

    @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">
                         Hi there, regular user
                    </div>
                </div>
            </div>
        </div>
    </div>
    @endsection

设置路由

我们的应用程序准备差不多了。让我们定义到目前为止创建的所有方法的路由。打开 routes/web.php 并替换为:

    // routes/web.php

    <?php
    Route::view('/', 'welcome');
    Auth::routes();

    Route::get('/login/admin', 'Auth\LoginController@showAdminLoginForm');
    Route::get('/login/writer', 'Auth\LoginController@showWriterLoginForm');
    Route::get('/register/admin', 'Auth\RegisterController@showAdminRegisterForm');
    Route::get('/register/writer', 'Auth\RegisterController@showWriterRegisterForm');

    Route::post('/login/admin', 'Auth\LoginController@adminLogin');
    Route::post('/login/writer', 'Auth\LoginController@writerLogin');
    Route::post('/register/admin', 'Auth\RegisterController@createAdmin');
    Route::post('/register/writer', 'Auth\RegisterController@createWriter');

    Route::view('/home', 'home')->middleware('auth');
    Route::view('/admin', 'admin');
    Route::view('/writer', 'writer');

修改我们的用户在经过身份验证后的重定向

当用户经过身份验证时,修改用户的重定向方式非常重要。默认情况下,Laravel将所有经过身份验证的用户重定向到  /home。如果不修改重定向,将会得到下面的错误。

所以, 为了解决这个, 打开 app/Http/Controllers/Middleware/RedirectIfAuthenticated.php 文件并替换为:

    // app/Http/Controllers/Middleware/RedirectIfAuthenticated.php

    <?php

    namespace App\Http\Middleware;

    use Closure;
    use Illuminate\Support\Facades\Auth;

    class RedirectIfAuthenticated
    {
        public function handle($request, Closure $next, $guard = null)
        {
            if ($guard == "admin" && Auth::guard($guard)->check()) {
                return redirect('/admin');
            }
            if ($guard == "writer" && Auth::guard($guard)->check()) {
                return redirect('/writer');
            }
            if (Auth::guard($guard)->check()) {
                return redirect('/home');
            }

            return $next($request);
        }
    }

RedirectIfAuthenticated 中间件接收 auth 守卫作为参数。当我们试图访问任何针对经过身份验证的用户的页面时,将触发此中间件。然后,我们可以确定用户拥有的身份验证类型并相应地重定向它们。

修改身份验证异常处理程序

当用户被重定向时会发生一件烦人的事情。如果用户试图访问 /writer,但没有经过身份验证,那么用户会被重定向到 /login/writer,他们被重定向到 /login 这不是我们想要的。

为了确保当用户尝试访问 /writer 时,它们被重定向到 /login/writer 或者同样用于 /admin,我们必须修改异常处理程序。 在 app/Exceptions 中打开处理程序文件并添加以下内容:

    // app/Exceptions/Handler.php

    <?php

    namespace App\Exceptions;

    use Exception;
    use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
    [...]
    use Illuminate\Auth\AuthenticationException;
    use Auth; 
    [...]
    class Handler extends ExceptionHandler
    {
       [...] 
        protected function unauthenticated($request, AuthenticationException $exception)
        {
            if ($request->expectsJson()) {
                return response()->json(['error' => 'Unauthenticated.'], 401);
            }
            if ($request->is('admin') || $request->is('admin/*')) {
                return redirect()->guest('/login/admin');
            }
            if ($request->is('writer') || $request->is('writer/*')) {
                return redirect()->guest('/login/writer');
            }
            return redirect()->guest(route('login'));
        }
    }

我们刚刚添加的 unauthenticated 方法解决了我们遇到的这个问题。 它默认接收一个 AuthenticationExpection 异常,它携带该保护信息。 遗憾的是,我们无法访问它,因为它受到保护(希望Laravel 5.7能够提供访问它的方法)。

我们的解决方法是使用 request→is()。这会检查我们尝试访问的URL。如果我们没有绝对URL或者我们有路由组,它也可以检查URL模式。

在我们的例子中,我们首先检查是否收到了 JSON 请求并单独处理异常。 然后我们检查我们是否正在尝试访问 /admin 或任何以 admin 开头的 URL。 我们将用户重定向到相应的登录页面。 我们也检查 writer

这对我们来说是一个很好的解决方法,但这意味着我们必须知道我们想要访问的绝对URL,或者至少对我们的守卫保护的所有路由都有相同的前缀。

运行应用程序

现在我们的应用程序已经准备好了,运行下面的命令来启动它:

    $ php artisan serve

它通常应该是可用的 http://localhost:8000.

先通过访问 http://localhost:8000/register/writer 和 http://localhost:8000/register/admin 这两个链接来分别注册作者和管理员. 然后访问 http://localhost:8000/login/writer 和 http://localhost:8000/login/admin 这两个链接分别登录作者和管理员。

结语

在本教程中,我们深入研究了 Laravel 身份验证。 我们定义了多个守卫来处理多个身份验证和访问控制。我们还为经过身份验证的用户处理重定向,为未经身份验证的用户处理重定向。

如果您完全遵循本指南,您将能够为具有不同用户类型的应用程序(可能是[多租户](https://en.wikipedia.org/wiki/Multitenancy)应用程序)设置基本身份验证。 尽管如此,尝试扩展你所看到的并分享你想出的东西。

本文中应用程序的源代码可在 GitHub

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://www.pusher.com/tutorials/multipl...

译文地址:https://learnku.com/laravel/t/26947

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
《L05 电商实战》
从零开发一个电商项目,功能包括电商后台、商品 & SKU 管理、购物车、订单管理、支付宝支付、微信支付、订单退款流程、优惠券等
《L04 微信小程序从零到发布》
从小程序个人账户申请开始,带你一步步进行开发一个微信小程序,直到提交微信控制台上线发布。
讨论数量: 2

@Summer 方便加上邮件和忘记密码相关的Auth吗?

4年前 评论

大佬,怎么两个看守器,一个登录另一个就登录了,有两个应用

3年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!