身份认证

简介

想要快速开始?只需在新的 Laravel 应用上运行 php artisan make:authphp artisan migrate。然后,将浏览器导航到 http://your-app.test/register 或其它分配给应用的 URL 就行了。这两个命令将搭建好整个认证系统。

Laravel 使实现身份认证变得非常简单。事实上,几乎所有东西都为您配置好了。身份认证配置文件位于 config/auth.php,其中包含了几个用于调整认证服务的注释清晰的选项。

Laravel 身份认证的核心由「看守器」和「提供者」组成。看守器定义了如何认证每个请求中的用户。例如,Laravel 自带了一个使用 Session 存储和 Cookies 来维持状态的 session 看守器。

提供者定义了如何从持久化的存储中获取用户。Laravel 默认支持使用 Eloquent 和数据库查询构造器来获取用户。当然,您可以根据应用需要自由定义其它的提供者。

不要担心这些听起来有点混乱!很多应用永远不需要修改默认的身份认证配置。

数据库注意事项

默认情况下,Laravel 在 app 目录中包含了一个 App\User Eloquent 模型。此模型会和默认的 Eloquent 认证驱动一起使用。如果应用不使用 Eloquent,可以使用 database 认证驱动,此驱动使用 Laravel 查询构造器。

当为 App\User 模型构建数据库表结构时,确保密码字段长度至少为 60 个字符。保持默认字段长度为 255 个字符是个不错的选择。

此外,要验证的 users (或等效的)数据表应该包含一个可为空的、长度为 100 的字符串字段 remember_token。此字段会用于存储当用户登录应用并勾选「记住我」选项时的用户令牌。

认证快速上手

Laravel 自带了一个预先构建的认证控制器,位于 App\Http\Controllers\Auth 命名空间下。RegisterController 处理新用户注册,LoginController 处理用户认证,ForgotPasswordController 处理重置密码时发送邮件链接,以及 ResetPasswordController 包含重置密码的逻辑。所有这些控制器都使用 Trait 来引入必要的方法。对于很多应用而言,根本不需要修改这些控制器。

路由

Laravel 提供了一个快速的方法来构建认证所需的所有路由和视图,使用一个简单的命令:

php artisan make:auth

此命令最好在新的应用中使用,它会安装布局视图、注册和登录视图,以及所有认证需要的路由。还会生成一个 HomeController 来处理登录应用仪表盘后的请求。

视图

如上一节所述,php artisan make:auth 命令会创建认证所需的所有视图,并将其放在 resources/views/auth 目录中。

make:auth 命令也会创建一个包含应用基本布局的 resources/views/layouts 目录。所有这些视图都使用 Bootstrap CSS 框架,您也可以自由修改它们。

认证

既然已经为自带的认证控制器设置好了路由和视图,就可以为应用注册和认证新用户了!可以在浏览器中访问应用,因为认证控制器已包含了认证已存在用户和存储新用户到数据库的逻辑。

自定义路径

当用户成功认证后,他们会被重定向到 /home URI。可以通过在 LoginControllerRegisterControllerResetPasswordControllerVerificationController 中定义 redirectTo 属性来自定义认证后的重定向位置:

protected $redirectTo = '/';

接下来,应该修改 RedirectIfAuthenticated 中间件的 handle 方法在重定向用户时使用新的 URI。

如果重定向路径需要自定义生成逻辑,可以定义 redirectTo 方法而不是 redirectTo 属性:

protected function redirectTo()
{
    return '/path';
}

redirectTo 方法会优先于 redirectTo 属性。

自定义用户名

默认情况下,Laravel 使用 email 字段进行用户认证。如果要自定义它,可以在 LoginController 中定义 username 方法:

public function username()
{
    return 'username';
}

自定义看守器

也可以自定义用于认证和注册用户的「看守器」。要实现该功能,在 LoginControllerRegisterControllerResetPasswordController 中定义 guard 方法。此方法应该返回一个看守器实例:

use Illuminate\Support\Facades\Auth;

protected function guard()
{
    return Auth::guard('guard-name');
}

自定义验证/存储

如果要修改新用户注册应用时所需的表单字段,或者自定义如何将新用户存储到数据库中,可以修改 RegisterController 类。该类负责验证和创建应用的新用户。

RegisterControllervalidator 方法包含了应用的新用户的验证规则。您可以根据需要自由修改该方法。

RegisterControllercreate 方法负责使用 Eloquent ORM 在数据库中创建新的 App\User 记录。您可以根据数据库需要自由修改该方法。

获取认证用户

可以通过 Auth Facade 获取认证用户:

use Illuminate\Support\Facades\Auth;

// 获取当前认证用户
$user = Auth::user();

// 获取当前认证用户的 ID
$id = Auth::id();

或者,当用户认证后,可以通过 Illuminate\Http\Request 实例来获取认证用户。请记住,类型提示的类会自动注入到控制器方法中:

namespace App\Http\Controllers;

use Illuminate\Http\Request;

class ProfileController extends Controller
{
    /**
     * 更新用户配置信息
     *
     * @param  Request  $request
     * @return Response
     */
    public function update(Request $request)
    {
        // $request->user() 返回认证用户的实例
    }
}

判断当前用户是否已认证

要判断用户是否已登录应用,可以使用 Auth Facade 的 check 方法,如果用户已认证会返回 true

use Illuminate\Support\Facades\Auth;

if (Auth::check()) {
    // 用户已登录
}

尽管可以使用 check 方法来判断用户是否已认证,但通常会在允许用户访问某个路由/控制器前使用中间件来验证用户是否已认证。要了解更多有关信息,请查看 保护路由 的文档。

保护路由

路由中间件 可用于仅允许认证用户访问给定路由。Laravel 自带 auth 中间件,它定义在 Illuminate\Auth\Middleware\Authenticate。由于此中间件已经在 HTTP 内核中注册了,因此只需要将中间件添加到路由定义中:

Route::get('profile', function () {
    // 只有认证用户才可以进入
})->middleware('auth');

当然,如果使用的是 控制器,可以在控制器的构造函数中调用 middleware 方法,而不是直接在路由中定义:

public function __construct()
{
    $this->middleware('auth');
}

重定向未认证用户

auth 中间件检测到未认证用户时,它会将用户重定向到 login 命名路由。可以在 app/Http/Middleware/Authenticate.php 文件中更新 redirectTo 函数来修改此行为:

/**
 * 获取用户应该被重定向的路径
 *
 * @param  \Illuminate\Http\Request  $request
 * @return string
 */
protected function redirectTo($request)
{
    return route('login');
}

指定看守器

当添加 auth 中间件到路由时,还可以指定用于认证用户的看守器。指定的看守器应该和 auth.php 配置文件的 guards 数组中的键名之一对应:

public function __construct()
{
    $this->middleware('auth:api');
}

登录限制

如果使用 Laravel 内置的 LoginController 类,控制器中已经引入了 Illuminate\Foundation\Auth\ThrottlesLogins Trait。默认情况下,如果用户在多次尝试后仍然没有提供正确的凭证,该用户将在一分钟内无法进行登录。该限制基于唯一的用户名/电子邮件地址和 IP 地址。

手动认证用户

当然,不一定非要使用 Laravel 自带的认证控制器。如果选择删除这些控制器,则需要直接使用 Laravel 的认证类来管理用户认证。别担心,很简单的!

我们要通过 Auth Facade 来获取 Laravel 的认证服务,因此确保在类的顶部引入了 Auth Facade。接下来,我们看一下 attempt 方法:

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

class LoginController extends Controller
{
    /**
     * 尝试进行认证
     *
     * @param  \Illuminate\Http\Request $request
     *
     * @return Response
     */
    public function authenticate(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            // 认证通过
            return redirect()->intended('dashboard');
        }
    }
}

attempt 方法接收一个键/值对数组作为其第一个参数。这个数组的值将用于在数据表中查找用户。因此,在上述示例中,会通过 email 字段的值来获取用户。找到用户后,会将数据库中存储的哈希密码和通过数组传递给该方法的 password 进行比较。您不用对 password 进行哈希处理,因为框架会在和数据库中哈希密码比较前自动对其进行哈希处理。如果两个哈希密码匹配,就会为用户开启一个已认证的会话。

如果认证成功,attempt 方法会返回 true。否则,返回 false

重定向的 intended 方法会将用户重定向到被认证中间件打断前他们要访问的 URL。如果要访问的路径不存在,可能还要给该方法指定一个备用的 URI。

指定其它条件

如果需要,除了用户的电子邮箱和密码还可以增加其它认证查询的条件。例如,我们要验证标记为「active」的用户:

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // 用户已激活,没有封停,并且存在
}

在这些示例中,email 不是必需的选项,仅作为示例使用。您应该使用与数据库的「用户名」对应的任何字段名。

使用指定看守器实例

使用 Auth Facade 的 guard 方法时,可以指定使用哪个看守器实例。这允许您使用完全独立的认证模型和用户表来管理应用不同部分的认证。

传递给 guard 方法的看守器名称应该和 auth.php 配置文件中配置的看守器之一对应:

if (Auth::guard('admin')->attempt($credentials)) {
    //
}

退出登录

要将用户从应用中退出登录,可以使用 Auth Facade 的 logout 方法。这会清除用户 Session 中的认证信息:

Auth::logout();

记住用户

如果要在应用中提供「记住我」的功能,可以将布尔值作为第二个参数传递给 attempt 方法,这会让用户一直保持认证身份,直到他们手动退出登录。当然,users 表必须包含字符串字段 remember_token,它用于存储「记住我」令牌。

if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) {
    // 用户被记住了
}

如果使用 Laravel 自带的 LoginController,「记住」用户的逻辑已由控制器使用的 Trait 实现了。

如果是「记住」用户,可以使用 viaRemember 方法来判断用户是否使用「记住我」Cookie 进行认证:

if (Auth::viaRemember()) {
    //
}

其它认证方法

认证用户实例

如果需要在应用中登录已有用户实例,可以和用户实例一起调用 login 方法。给定的对象必须是 Illuminate\Contracts\Auth\Authenticatable Contract 的实现。当然,Laravel 包含的 App\User 已经实现了此接口:

Auth::login($user);

// 登录并「记住」给定用户
Auth::login($user, true);

当然,也可以指定要使用的看守器实例:

Auth::guard('admin')->login($user);

通过 ID 认证用户

要通过用户 ID 将用户登录到应用,可以使用 loginUsingId 方法。此方法接收想要认证的用户的主键:

Auth::loginUsingId(1);

// 登录并「记住」给定用户
Auth::loginUsingId(1, true);

单次认证用户

可以使用 once 方法为单个请求登录用户到应用。不会使用 Session 或 Cookie,这意味着当构建无状态的 API 时此方法可能会很有用:

if (Auth::once($credentials)) {
    //
}

HTTP 基础认证

HTTP 基础认证 为应用提供了一种快捷的方式来认证用户,而不用设置专门的「登录」页面。要实现此功能,将 auth.basic 中间件 添加到路由。由于 Laravel 包含了 auth.basic 中间件,因此不用再定义它:

Route::get('profile', function () {
    // 只有认证用户可以进入
})->middleware('auth.basic');

中间件添加到路由后,在浏览器中访问该路由时,就会自动提示您输入凭证。默认情况下,auth.basic 中间件会使用用户的 email 字段作为「用户名」。

FastCGI 注意事项

当使用 PHP FastCGI 时,HTTP 基础认证可能无法正常工作。应该将下面几行添加到 .htaccess 文件中:

RewriteCond %{HTTP:Authorization} ^(.+)$
RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]

无状态 HTTP 基础认证

也可以使用 HTTP 基础认证,而不用在 Session 中设置用户标识符的 Cookie。为此,要定义一个调用 onceBasic 方法的 中间件。如果 onceBasic 方法不返回任何响应,那么该请求会被进一步传入应用:

namespace App\Http\Middleware;

use Illuminate\Support\Facades\Auth;

class AuthenticateOnceWithBasicAuth
{
    /**
     * 处理传入的请求
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, $next)
    {
        return Auth::onceBasic() ?: $next($request);
    }

}

接下来,注册路由中间件,并将其添加到路由:

Route::get('api/user', function () {
    // 只有认证用户可以进入
})->middleware('auth.basic.once');

退出登录

要手动将用户从应用退出登录,可以使用 Auth Facade 的 logout 方法。它会清除用户 Session 中的认证信息:

use Illuminate\Support\Facades\Auth;

Auth::logout();

让其它设备上的 Session 失效

Laravel 也提供了一个方法,使用户在其它设备上激活的 Session 失效并「退出登录」,但不会让当前设备上的 Session 失效。要实现该功能,应确保 Illuminate\Session\Middleware\AuthenticateSession 中间件存在,并在 app/Http/Kernel.php 类的 web 中间件组中没有被注释掉:

'web' => [
    // ...
    \Illuminate\Session\Middleware\AuthenticateSession::class,
    // ...
],

然后,可以使用 Auth Facade 的 logoutOtherDevices 方法。此方法要求用户提供当前的密码,应用可以通过表单接收该密码:

use Illuminate\Support\Facades\Auth;

Auth::logoutOtherDevices($password);

当调用 logoutOtherDevices 方法时,用户的其它 Session 会完全失效,这意味着它们会从之前认证通过的所有看守器「退出登录」。

添加自定义看守器

可以使用 Auth Facade 的 extend 方法定义自己的认证看守器。您应该在 服务提供者 中调用 extend。由于 Laravel 已经自带了 AuthServiceProvider,我们可以将代码放在里面:

namespace App\Providers;

use App\Services\Auth\JwtGuard;
use Illuminate\Support\Facades\Auth;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 注册任何应用认证/授权服务
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::extend('jwt', function ($app, $name, array $config) {
            // 返回一个 Illuminate\Contracts\Auth\Guard 的实例

            return new JwtGuard(Auth::createUserProvider($config['provider']));
        });
    }
}

如上述示例所示,传递给 extend 方法的回调应该返回一个 Illuminate\Contracts\Auth\Guard 的实现。该接口包含了定义自定义看守器所需的一些方法。定义了自定义看守器之后,就可以使用 auth.php 配置文件的 guards 中配置的看守器了:

'guards' => [
    'api' => [
        'driver' => 'jwt',
        'provider' => 'users',
    ],
],

闭包请求看守器

实现自定义、基于 HTTP 请求的认证系统的最简单方式是使用 Auth::viaRequest 方法。该方法允许您使用一个闭包快速定义认证过程。

要实现该功能,在 AuthServiceProviderboot 方法中调用 Auth::viaRequest 方法。viaRequest 方法接收一个看守器名称作为其第一个参数。该名称可以是任何描述自定义看守器的字符串。第二个传递给该方法的参数应该是一个闭包,该闭包接收一个传入的 HTTP 请求,并返回一个用户实例,或者如果认证失败,返回 null

use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;

/**
 * 注册任何应用的认证/授权服务
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();

    Auth::viaRequest('custom-token', function ($request) {
        return User::where('token', $request->token)->first();
    });
}

定义自定义看守器之后,就可以使用 auth.php 配置文件的 guards 配置中的看守器:

'guards' => [
    'api' => [
        'driver' => 'custom-token',
    ],
],

添加自定义用户提供者

如果不使用传统的关系型数据库来存储用户,则需要用您自己的认证用户提供者来扩展 Laravel。可以使用 Auth Facade 的 provider 方法定义一个自定义用户提供者:

namespace App\Providers;

use Illuminate\Support\Facades\Auth;
use App\Extensions\RiakUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;

class AuthServiceProvider extends ServiceProvider
{
    /**
     * 注册任何应用的认证/授权服务
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();

        Auth::provider('riak', function ($app, array $config) {
            // 返回一个 Illuminate\Contracts\Auth\UserProvider 的实例

            return new RiakUserProvider($app->make('riak.connection'));
        });
    }
}

使用 provider 方法注册提供者后,就可以在 auth.php 配置文件中切换到新的用户提供者。首先,使用新的驱动定义 provider

'providers' => [
    'users' => [
        'driver' => 'riak',
    ],
],

最后,可以在 guards 配置中使用此用户提供者:

'guards' => [
    'web' => [
        'driver' => 'session',
        'provider' => 'users',
    ],
],

用户提供者 Contract

Illuminate\Contracts\Auth\UserProvider 的实现只负责从持久化存储系统中(如 MySQL,Riak 等)获取 Illuminate\Contracts\Auth\Authenticatable 的实现。这两个接口确保 Laravel 认证机制正常运行,而不用管用户数据是如何被存储或者使用什么类代表它。

让我们看一看 Illuminate\Contracts\Auth\UserProvider Contract:

namespace Illuminate\Contracts\Auth;

interface UserProvider {

    public function retrieveById($identifier);
    public function retrieveByToken($identifier, $token);
    public function updateRememberToken(Authenticatable $user, $token);
    public function retrieveByCredentials(array $credentials);
    public function validateCredentials(Authenticatable $user, array $credentials);

}

retrieveById 函数通常接收一个代表用户的键,例如一个 MySQL 数据库的自增长 ID。获取匹配该 ID 的 Authenticatable 的实现并返回。

retrieveByToken 函数接收一个通过唯一的 $identifier 和存储在 remember_token 字段的「记住我」的 $token 获取的用户。和之前的方法一样,应该返回一个 Authenticatable 的实现。

updateRememberToken 方法使用新的 $token 更新 $userremember_token 字段。当「记住我」登录成功后或用户退出登录时指定新的令牌。

retrieveByCredentials 方法接收一个数组凭证,当尝试登录应用时传递给 Auth::attempt 方法。然后该方法会「查询」底层持久化存储中匹配上述凭据的用户。通常,该方法会对 $credentials['username'] 运行一个带「where」条件的查询语句。然后应该返回一个 Authenticatable 的实现。此方法不应尝试进行任何密码验证或认证。

validateCredentials 方法应该比较给定 $user 和要认证的用户 $credentials。例如,该方法可能使用 Hash::check 来比较 $user->getAuthPassword()$credentials['password'] 的值。根据密码是否有效返回 truefalse

认证 Contract

现在我们已经研究了 UserProvider 的每个方法,让我们再来看一下 Authenticatable Contract。请记住,用户提供器应该从 retrieveByIdretrieveByTokenretrieveByCredentials 方法返回此接口的实现:

namespace Illuminate\Contracts\Auth;

interface Authenticatable {

    public function getAuthIdentifierName();
    public function getAuthIdentifier();
    public function getAuthPassword();
    public function getRememberToken();
    public function setRememberToken($value);
    public function getRememberTokenName();

}

这个接口很简单。getAuthIdentifierName 方法应该返回用户的「主键」字段名,getAuthIdentifier 方法应该返回用户的「主键」。同样,在 MySQL 中,它会是自增长的主键。getAuthPassword 应该返回用户的哈希密码。此接口允许认证系统使用任何用户类,而不用管使用的是 ORM 还是存储抽象层。默认情况下,Laravel 的 app 目录中包含的 User 类实现了该接口,因此可以将其作为一个实现示例参考。

事件

Laravel 在认证过程中触发了多种 事件。可以在 EventServiceProvider 中给这些事件添加监听器:

/**
 * 应用的事件监听器映射
 *
 * @var array
 */
protected $listen = [
    'Illuminate\Auth\Events\Registered' => [
        'App\Listeners\LogRegisteredUser',
    ],

    'Illuminate\Auth\Events\Attempting' => [
        'App\Listeners\LogAuthenticationAttempt',
    ],

    'Illuminate\Auth\Events\Authenticated' => [
        'App\Listeners\LogAuthenticated',
    ],

    'Illuminate\Auth\Events\Login' => [
        'App\Listeners\LogSuccessfulLogin',
    ],

    'Illuminate\Auth\Events\Failed' => [
        'App\Listeners\LogFailedLogin',
    ],

    'Illuminate\Auth\Events\Logout' => [
        'App\Listeners\LogSuccessfulLogout',
    ],

    'Illuminate\Auth\Events\Lockout' => [
        'App\Listeners\LogLockout',
    ],

    'Illuminate\Auth\Events\PasswordReset' => [
        'App\Listeners\LogPasswordReset',
    ],
];