用户认证


简介

许多 Web 应用为用户提供了通过登录进行认证的方式。在 Web 应用中实现这个功能是一项复杂且有潜在安全风险的工作。出于这个原因,Laravel 努力为你提供快速、安全、简单的用户认证实现工具。

在底层代码中,Laravel 的认证组件由「guards」和「providers」组成,Guard 定义了用户在每个请求中如何实现认证,例如,Laravel 通过 session guard 来维护 Session 存储的状态和 Cookie。

Provider 定义了如何从持久化存储中获取用户信息,Laravel 底层支持通过 Eloquent 和数据库查询构建器两种方式来获取用户,如果需要的话,你还可以定义额外的 Provider。

如果看到这些名词觉得云里雾里,大可不必太过担心,因为对绝大多数应用而言,只需使用默认认证配置即可,不需要做什么改动。实际上,几乎所有东西 Laravel 都已经为你配置好了。配置文件位于 config/auth.php,其中包含了用于调整认证服务行为的、文档友好的选项配置。

学院君注:通俗点说,在进行登录认证的时候,要做两件事,一个是从数据库存取用户数据,一个是把用户登录状态保存起来,在 Laravel 的底层实现中,通过 Provider 存取数据,通过 Guard 存储用户认证信息,前者主要和数据库打交道,后者主要和 Session 打交道(API 例外)。

入门套件

想要在新建的 Laravel 项目中快速实现用户认证功能?这可以通过安装官方提供的 入门套件扩展包 扩展包来实现。完成数据库迁移工作后,在浏览器中访问 /register 即可进入新用户注册页面进行体验。入门套件已经为完整的用户认证系统提供了所有必要的脚手架代码。

即使你选择不在你的 Laravel 应用中使用入门套件扩展包,安装Laravel Breeze 扩展包也是一个很好的机会来学习如何在一个实际的 Laravel 项目中实现所有认证功能,因为 Laravel Breeze 为你创建了认证相关的控制器、路由和视图,你可以查看这些文件中的实现代码来学习如何实现 Laravel 的认证功能。

数据库考量

默认情况下,Laravel 在 app/Models 目录下包含了一个 Eloquent 模型 App\Models\User,这个模型可以和默认的 Eloquent 认证驱动一起使用。如果你的应用不使用 Eloquent,你可以使用 database 认证驱动,该驱动使用 Laravel 查询构建器与数据库交互。

App\Models\User 模型构建数据库表结构的时候,确保 password 字段长度至少有 60 位。保持默认字符串长度(255)是个不错的选择。

还有,你需要验证 users 表包含了 remember_token,该字段是个可以为空的字符串类型,字段长度为 100,用于在登录时存储应用维护的「记住我」 Session 令牌。

-w740

生态系统概览

Laravel 框架提供了多个认证相关的官方扩展包,接下来,我们来整体探究一下 Laravel 的认证生态系统并讨论每个扩展包的预期目标。

首先,来看看认证的原理。使用 Web 浏览器认证的时候,需要用户通过登录表单提供用户名和密码作为凭证,如果这些凭证是正确的,Laravel 应用会在用户 Session 中存储该认证用户的信息,然后将包含 Session ID 的 Cookie 通过响应头发送给客户端浏览器,以便后续该浏览器发起的 HTTP 请求可以通过 Session 与指定用户关联起来:服务端获取到带有 Session Cookie 的请求后,会基于 Session ID 获取 Session 数据,由于之前在 Session 中存储了认证用户信息,所以通过这种机制就可以将发起该请求的用户看作是「认证用户」。这样一个原本「无状态」的 HTTP 通信就变成「有状态的」了。

当一个远程服务需要认证后才访问 API 接口时,由于没有 Web 浏览器,所以就不能使用 Cookie 在客户端和服务端之间传递 Session ID 了,取而代之的,我们可以在每个请求中带上远程服务颁发的 API 访问令牌进行认证。服务端会验证令牌是否有效,如果有效的话就可以将该请求与数据表中持有该令牌的用户关联起来,实现认证的效果。

Laravel 内置的浏览器认证服务

Laravel 通过 AuthSession 门面提供了内置的认证和会话服务,这些功能提供了基于 Cookie 的浏览器请求认证,你可以使用这些门面提供的方法验证用户登录凭证然后对用户进行认证操作。此外,这些服务还会自动存储相应的认证数据到用户 Session 并通过 HTTP 响应发送包含对应 Session ID 的 Cookie。这篇文档后续部分包含了如何使用这些服务。

应用入门套件

正如本文档后面所介绍的,你可以通过与 Laravel 内置认证服务交互来手动构建自定义的用户认证层,不过,为了帮助你快速起步,Laravel 官方发布了免费的认证扩展包,它们提供了强大的、现代的、完整的认证层代码实现,这些扩展包就是 Laravel BreezeLaravel JetstreamLaravel Fortify

Laravel Breeze 是一个简单的、最小化的完整认证功能实现扩展包,包含了登录、注册、密码重置、邮箱验证和密码确认等功能。Laravel Breeze 的视图层使用的是 Blade 模板Tailwind CSS,要使用这个扩展包,请参考 Laravel 应用入门套件文档

Laravel Fortify 是一个针对 Laravel 项目开发的开箱即用认证后端,实现了本文档介绍的大部分功能,包括基于 Cookie 的认证以及双因子认证和邮箱验证。Fority 是 Laravel Jetstream 认证功能的后端实现,也可以独立与 Laravel Sanctum 结合用于为 Laravel 单页面应用提供认证功能。

Laravel Jetstream 是一个功能强大的入门套件扩展包,基于 Tailwind CSSLaravel Livewire 以及 Inertia.js 等前端技术栈为 Laravel Fortify 的后端认证服务提供了美观的、现代的 UI 界面。Laravel Jetstream 除了提供基于浏览器的 Cookie 认证外,还内置集成了 Laravel Sanctum 提供 API 令牌认证。Laravel 的 API 认证下面马上就会介绍。

Laravel API 认证服务

Laravel 官方提供了两个可选的扩展包帮助你管理 API 令牌和认证带有 API 令牌的请求:PassportSanctum。这两个库和 Laravel 内置的基于 Cookie 的认证库并不互斥,因为这些库主要关注 API 令牌认证而内置的认证服务关注的是基于 Cookie 的浏览器认证。很多应用会同时使用 Laravel 内置的基于 Cookie 的浏览器认证服务和任意一个 Laravel API 认证扩展包(Passport 和 Sanctum 二选一)。

Passport

Passport 是一个 OAuth2 认证服务商,提供了多个 OAuth2「授权类型」以便颁发不同类型的访问令牌。总体来说,它是一个健全且复杂的 API 认证扩展包。不过,大多数应用并不需要 OAuth2 规范提供的复杂特性,这会让开发者和用户都感到困惑。此外,开发者也一直对如何使用 Passport 认证 SPA 应用和移动应用感到困扰。

Sanctum

针对 OAuth2 的复杂和开发者的困扰,Laravel 官方开始着手构建更加简单、更加精练的认证扩展包用于处理来自浏览器的第一方 Web 请求和基于令牌的 API 请求,这个目标最终被落地为 Laravel Sanctum 的发布。对于除了 API 接口之外还提供第一方 Web UI 的应用、或者前后端分离的单页面应用、以及带有移动客户端的 Laravel 应用,优先推荐使用 Sanctum 这个认证扩展包。

Laravel Sanctum 是一个混合了 Web/API 认证的扩展包,可用于管理应用的整个认证流程。其背后的工作原理是对于一个基于 Sanctum 提供认证服务的应用,当服务端接收到请求时,Sanctum 会先判断请求是否包含引用了认证 Session 的会话 Cookie,如果没有通过会话 Cookie 认证,Sanctum 会继续检查请求是否包含 API 令牌,如果 API 令牌存在,则 Sanctum 会使用 API 令牌认证请求。想要了解更多关于这个处理流程的底层细节,请参考 Sanctum 官方文档

Laravel Jetstream 认证脚手架代码中使用了 Laravel Sanctum 扩展包提供 API 认证服务,因为我们相信它能够满足大多数 Web 应用的认证需求。

总结 & 如何选择

总而言之,如果你的应用只会通过浏览器访问,使用 Laravel 内置的认证服务就好了。

接下来,如果应用提供了 API 接口,可以在 PassportSanctum 扩展包中任选其一提供 API 令牌认证。一般来说,优先使用 Sanctum,因为它位 API 认证、SPA 认证以及移动端认证提供了简单但完整的解决方案,包括对「作用域」和「权限」的支持。

如果你想要构建基于 Laravel 后端驱动的单页面应用(SPA),可以使用 Laravel Sanctum,使用 Sanctum 的时候,可以手动手动实现后端认证路由,也可以使用 Laravel Fority 作为后端认证服务提供者。

Passport 可用于构建基于 OAuth2 规范的认证功能,比如我们要做开放平台,需要提供针对第三方应用的授权认证(比如微信、支付宝、QQ、微博之类的开发平台),则只能选择 Passport。

最后,如果你想要在新安装的 Laravel 项目中快速搭建认证系统,推荐使用 Laravel Jetstream,其中包含了 Laravel 内置的认证服务(通过 Laravel Fortify 集成)和 Laravel Sanctum 提供完整的用户认证解决方案。

快速入门

这部分文档的认证功能实现由 Laravel 应用入门套件扩展包实现,所以已经包含了 UI 脚手架代码,如果你想要直接与 Laravel 底层认证服务整合,请参考对应的手动认证用户文档

安装入门套件

参考入门套件教程选择一个扩展包和安装即可。

获取认证用户

你可以通过 Auth 门面访问认证用户:

use Illuminate\Support\Facades\Auth;

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

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

此外,用户通过认证后,你还可以通过 Illuminate\Http\Request 实例访问认证用户(类型提示类会通过依赖注入自动注入到控制器方法中):

<?php
    
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 门面的 check 方法,如果用户通过认证则返回 true

use Illuminate\Support\Facades\Auth;

if (Auth::check()) {
    // The user is logged in...
}

注:尽管我们可以使用 check 方法判断用户是否通过认证,但是我们通常的做法是在用户访问特定路由/控制器之前使用中间件来验证用户是否通过认证,想要了解更多,可以查看下面的路由保护

路由保护

路由中间件可用于只允许通过认证的用户访问给定路由。Laravel 通过定义在 Illuminate\Auth\Middleware\Authenticate 中的 auth 中间件来实现这一功能。由于该中间件已经在 HTTP kernel 中注册,你所要做的仅仅是将该中间件加到相应的路由定义中:

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

当然,如果你也可以在控制器的构造方法中调用 middleware 方法而不是在路由器中直接定义实现同样的功能:

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

重定向未认证用户

auth 中间件判定某个用户未认证,会将用户重定向到 login 命名路由(也就是登录页面),你可以通过更新位于 app/Http/Middleware/Authenticate.php 文件中的 redirectTo 函数来调整这个行为:

/**
 * Get the path the user should be redirected to.
 *
 * @param  \Illuminate\Http\Request  $request
 * @return string
 */
protected function redirectTo($request)
{
    return route('login');
}

指定一个 Guard

添加 auth 中间件到路由后,还可以指定使用哪个 guard 来实现认证, 指定的 guard 对应配置文件 config/auth.phpguards 数组的某个键 :

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

如果没有指定的话,默认 guard 是 web,这也是配置文件中配置的:

'defaults' => [
    'guard' => 'web',
    'passwords' => 'users',
],

登录失败次数限制

如果你使用了 Laravel Jetstream,访问频率限制会自动应用到登录路由,默认情况下,如果登录失败次数超过指定阈值,用户将在一分钟内不能进行登录尝试:

-w665

这种限制基于用户的用户名/邮箱地址 + IP 地址来唯一确定客户端。

手动认证用户

当然,你也可以不使用 Laravel 入门套件扩展包自带的认证控制器。如果你选择不使用上述脚手架代码,需要直接使用 Laravel 认证类来管理用户认证。别担心,这很简单!

我们可以通过 Auth 门面来访问认证服务,因此我们需要确保在类的顶部导入了 Auth 门面,接下来,让我们看看如何通过 attempt 方法实现登录认证,attempt 方法用于处理用户表单提交数据的认证尝试,如果认证成功,会生成一个用户 Session 来避免会话固定攻击

<?php
    
namespace App\Http\Controllers;
    
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
    
class LoginController extends Controller
{
    /**
     * 处理登录认证
     *
     * @return Response
     * @translator laravelacademy.org
     */
    public function authenticate(Request $request)
    {
        $credentials = $request->only('email', 'password');

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();
            return redirect()->intended('dashboard');
        }

        return back()->withErrors([
            'email' => 'The provided credentials do not match our records.',
        ]);
    }
}

attempt 方法接收键/值对作为第一个参数,数组中的值被用于从数据表中查找对应用户。在上面的例子中,将会通过 email 的值作为查询条件去数据库获取对应用户,如果用户被找到,经哈希运算后存储在数据库中的密码将会和传递过来的经哈希运算处理的 password 值进行比较(这里开发者不需要主动对密码值进行哈希运算,Laravel 底层会在比较之前自动处理)。如果两个经哈希运算的密码相匹配,那么将会为这个用户设置一个认证 Session,标识该用户登录成功。感兴趣的同学可以去看下底层源码实现逻辑,对应源码位于 vendor/laravel/framework/src/Illuminate/Auth/SessionGuard.php 中:

/**
 * Attempt to authenticate a user using the given credentials.
 *
 * @param  array  $credentials
 * @param  bool   $remember
 * @return bool
 */
public function attempt(array $credentials = [], $remember = false)
{
    $this->fireAttemptEvent($credentials, $remember);

    $this->lastAttempted = $user = $this->provider->retrieveByCredentials($credentials);

    // If an implementation of UserInterface was returned, we'll ask the provider
    // to validate the user against the given credentials, and if they are in
    // fact valid we'll log the users into the application and return true.
    if ($this->hasValidCredentials($user, $credentials)) {
        $this->login($user, $remember);

        return true;
    }

    // If the authentication attempt fails we will fire an event so that the user
    // may be notified of any suspicious attempts to access their account from
    // an unrecognized user. A developer may listen to this event as needed.
    $this->fireFailedEvent($user, $credentials);

    return false;
}

记住,Laravel 的认证服务会基于认证守卫的「provider」配置从数据库获取用户信息,在配置文件 config/auth.php 中,现在指定的提供者是 Eloquent 用户模型类 —— App\Models\User,你可以按照业务需求对配置做调整。

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

登录成功后,重定向器上的 intended 方法会将用户重定向到登录之前用户想要访问的 URL,如果该 URL 无效,则使用传递给 intended 方法的 URI 作为兜底进行重定向,这里的兜底 URI 是 /dashboard

指定额外条件

如果需要的话,除了用户邮件和密码之外还可以在认证查询时添加额外的条件,例如,我们可以限定状态有效的用户才能进行登录认证:

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // The user is active, not suspended, and exists.
}

这里的实现原理是在查询用户记录时,只是排除了数组中的密码字段,其他字段都会作为查询条件之一进行筛选,对应的底层实现源码如下:

/**
 * Retrieve a user by the given credentials.
 *
 * @param  array  $credentials
 * @return \Illuminate\Contracts\Auth\Authenticatable|null
 */
public function retrieveByCredentials(array $credentials)
{
    if (empty($credentials) ||
       (count($credentials) === 1 &&
        array_key_exists('password', $credentials))) {
        return;
    }

    // First we will add each credential element to the query as a where clause.
    // Then we can execute the query and, if we found a user, return it in a
    // Eloquent User "model" that will be utilized by the Guard instances.
    $query = $this->newModelQuery();

    foreach ($credentials as $key => $value) {
        if (Str::contains($key, 'password')) {
            continue;
        }

        if (is_array($value) || $value instanceof Arrayable) {
            $query->whereIn($key, $value);
        } else {
            $query->where($key, $value);
        }
    }

    return $query->first();
}

注:在这些例子中,并不仅仅限于使用 email 进行登录认证,这里只是作为演示示例,你可以将其修改为数据库中任何其他可用作「username」的字段。

访问指定 Guard 实例

你可以使用 Auth 门面的 guard 方法指定想要使用的 guard 实例,这种机制允许你在同一个应用中对不同的认证模型或用户表实现完全独立的用户认证。

该功能可用于为不同表的不同类型用户(同一个表不同类型用户理论上也可以)实现隔离式登录提供了方便,我们只要为每张表配置一个独立的 guard 就可以了。比如我们除了 users 表之外还有一张 admins 表用于存放后台管理员,要实现管理员的单独登录,就可以这么配置 auth.php 配置文件:

'guards' => [
    ...

    'admin' => [
        'driver' => 'session',
        'provider' => 'admins',
    ]
],

'providers' => [
    ...

    'admins' => [
        'driver' => 'eloquent',
        'model' => App\Admin::class,
    ],
],

友情提示:新建的用于登录认证的模型类需要继承 Illuminate\Foundation\Auth\User 基类,不然后面就会出现不能认证的窘况。

传递给 guard 方法的 guard 名称对应配置文件 auth.phpguards 配置的 admin 键:

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

需要注意的是使用这种方式认证的用户在后续操作需要传递 guard 时也要传递相匹配的 guard,比如上面提到的 auth 中间件,对应的调用方式也要调整(在路由中使用也是一样):

$this->middleware('auth:admin'); 

获取用户时也是一样:

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

退出

要退出应用,可以使用 Auth 门面的 logout 方法,这将会清除用户 Session 中的认证信息,这样一来,后续请求的状态就是未认证的了:

Auth::logout();

除了调用 logout 方法外,推荐在当前请求处理中将用户 Session 标记为无效,并重新生成 CSRF 令牌。当用户退出后,通常会将其重定向到应用首页:

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

/**
 * Log the user out of the application.
 *
 * @param  \Illuminate\Http\Request $request
 * @return \Illuminate\Http\Response
 */
public function logout(Request $request)
{
    Auth::logout();

    $request->session()->invalidate();

    $request->session()->regenerateToken();

    return redirect('/');
}

记住用户

如果你想要在应用中提供“记住我”的功能,可以传递一个值为 true 的布尔值作为第二个参数到 attempt 方法(不传的话默认是 false),这样用户登录认证状态就会一直保持直到他们手动退出。当然,你的 users 表必须包含 remember_token 字段,该字段用于存储「记住我」令牌。

if (Auth::attempt(['email' => $email, 'password' => $password], $remember)) {
    // The user is being remembered...
}

如果你在使用「记住」用户功能,可以使用 viaRemember 方法来判断用户是否通过“记住我“Cookie进行认证:

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

其它认证方法

认证一个用户实例

如果你需要将一个已存在的用户实例直接登录到应用,可以调用 Auth 门面的 login 方法并传入用户实例,传入实例必须是 Illuminate\Contracts\Auth\Authenticatable 契约的实现,当然,Laravel 自带的 App\User 模型已经实现了该接口:

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

当然,你可以指定想要使用的 guard 实例:

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 自带的,所以不需要自己定义:

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,这在 API 认证中非常有用。要实现这个功能,需要定义一个调用 onceBasic 方法的中间件。如果该方法没有返回任何响应,那么请求会继续走下去:

<?php
    
namespace Illuminate\Auth\Middleware;
    
use Illuminate\Support\Facades\Auth;
    
class AuthenticateOnceWithBasicAuth
{
    /**
     * 处理输入请求.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     * @translator laravelacademy.org
     */
    public function handle($request, $next)
    {
        return Auth::onceBasic() ?: $next($request);
    }
    
}

接下来,将 AuthenticateOnceWithBasicAuth 注册到路由中间件并在路由中使用它:

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

退出

要退出应用,可以使用 Auth 门面的 logout 方法,这将会清除用户 Session 中的认证信息,这样一来,后续请求的状态就是未认证的了。

除了调用 logout 方法外,推荐在当前请求处理中将用户 Session 标记为无效,并重新生成 CSRF 令牌。当用户退出后,通常会将其重定向到应用首页:

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

/**
 * Log the user out of the application.
 *
 * @param  \Illuminate\Http\Request $request
 * @return \Illuminate\Http\Response
 */
public function logout(Request $request)
{
    Auth::logout();

    $request->session()->invalidate();

    $request->session()->regenerateToken();

    return redirect('/');
}

让其他设备上的 Session 失效

Laravel 还提供了让用户 Session 在除当前设备之外的其他登录设备上失效的机制,这个功能常用在用户修改或者更新密码这种场景,修改密码之后其他设备需要重新认证从而提高安全性。

使用这个功能之前,需要确保 Illuminate\Session\Middleware\AuthenticateSession 中间件在 app/Http/Kernel.php 类的 web 中间件组中存在且没有被注释:

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

然后,你可以使用 Auth 门面上的 logoutOtherDevices 方法实现在其他设备「退出」,该方法要求用户提供登录密码:

use Illuminate\Support\Facades\Auth;

Auth::logoutOtherDevices($password);

logoutOtherDevices 方法被调用时,用户在其他设备的 Session 会完全失效,表现在用户界面上就是退出登录了。

注:当结合使用 AuthenticateSession 中间件和 login 路由的自定义路由名称时,必须重写应用异常处理器上的 unauthenticated 方法,以便用户可以被正常重定向到登录页面。

密码确认

在构建应用时,可能偶尔会有一些操作需要用户在执行之前或者被重定向到应用的敏感区域之前确认他们的密码。Laravel 提供了内置的中间件让这个过程变得轻而易举。实现这个功能需要你定义两个路由:一个用于显示要求用户确认密码的视图,另一个用于确认密码有效并将用户重定向到预期的目的地。

下面的文档主要讨论如何直接整合 Laravel 内置的密码确认功能,如果你想要更快实现,可以使用 Laravel 应用入门套件

配置

用户确认密码后,三小时内不会要求再次确认密码,不过,你可以在用户输入密码进行确认前通过修改配置文件 config/auth.phppassword_timeout 配置项的值来调整这个时间长度。

路由

密码确认表单

首先,我们会定义一个路由来显示要求用户输入确认密码的表单视图:

Route::get('/confirm-password', function () {
    return view('auth.confirm-password');
})->middleware('auth')->name('password.confirm');

正如你所期望的,这个路由返回的视图需要有一个包含 password 字段的表单。此外,还可以在这个视图中引入文字来说明用户正在进入应用程序的受保护区域,必须确认密码。

确认密码

接下来,我们将定义一个路由,用于处理「确认密码」视图中的表单请求。这个路由将负责验证密码,并将用户重定向到他们的目的地:

use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Redirect;

Route::post('/confirm-password', function (Request $request) {
    if (! Hash::check($request->password, $request->user()->password)) {
        return back()->withErrors([
            'password' => ['The provided password does not match our records.']
        ]);
    }

    $request->session()->passwordConfirmed();

    return redirect()->intended();
})->middleware(['auth', 'throttle:6,1'])->name('password.confirm');

在继续后续操作之前,我们先来详细研究一下这个路由。首先,判断请求中的 password 字段是否与认证用户的密码相匹配。如果匹配,我们需要告知 Laravel Session 该用户已经确认了自己的密码。passwordConfirmed 方法会在用户 Session 中设置一个时间戳,Laravel 可以通过它来确定用户最后一次确认密码的时间。最后,我们将用户重定向到他们预期的目的地。

保护路由

你应该确保任何执行需要最近确认密码操作的路由都分配了 password.confirm 中间件。这个中间件包含在 Laravel 的默认安装中,它将自动在 Session 中存储用户的预期目的地,以便用户在确认密码后可以重定向到该位置。在 Session 中存储了用户的目的地后,中间件会将用户重定向到名为 password.confirm 的路由:

Route::get('/settings', function () {
    // ...
})->middleware(['password.confirm']);

Route::post('/settings', function () {
    // ...
})->middleware(['password.confirm']);

添加自定义 Guard 驱动

你可以通过 Auth 门面的 extend 方法定义自己的认证 guard 驱动,该功能需要在某个服务提供者boot 方法中实现,由于 Laravel 已经自带了一个 AuthServiceProvider,所以我们将代码放到这个服务提供者中:

<?php
    
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 的实现实例,该接口包含了自定义认证 guard 驱动需要的一些方法。定义好自己的认证 guard 驱动之后,就可以在配置文件 auth.phpguards 配置中使用这个新的 guard 驱动:

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

闭包请求 Guard

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

首先我们需要在 AuthServiceProviderboot 方法中调用 Auth::viaRequestviaRequest 方法接收一个 guard 名称作为第一个参数,这个名称可以是任意描述自定义 guard 的字符串,传递到该方法第二个参数应该是一个闭包,该闭包接收 HTTP 请求并返回一个用户实例,如果认证失败的话,则返回 null

use App\Models\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
    
/**
 * Register any application authentication / authorization services.
 *
 * @return void
 */
public function boot()
{
    $this->registerPolicies();
    
    Auth::viaRequest('custom-token', function ($request) {
        return User::where('token', $request->token)->first();
    });
}

定义好自定义 guard 后,就可以在 auth.php 配置文件的配置项 guards 中使用这个 guard 了:

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

添加自定义用户提供者

如果你没有使用传统的关系型数据库存储用户信息,则需要使用自己的认证用户提供者来扩展 Laravel。我们使用 Auth 门面上的 provider 方法定义自定义该提供者:

<?php

namespace App\Providers;
    
use App\Extensions\RiakUserProvider;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
use Illuminate\Support\Facades\Auth;
    
class AuthServiceProvider extends ServiceProvider
{
    /**
     * Register any application authentication / authorization services.
     *
     * @return void
     */
    public function boot()
    {
        $this->registerPolicies();
    
        Auth::provider('riak', function ($app, array $config) {
            // Return an instance of Illuminate\Contracts\Auth\UserProvider...
    
            return new RiakUserProvider($app->make('riak.connection'));
        });
    }
}

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

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

然后,可以在你的 guards 配置中使用这个提供者:

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

User Provider 契约

Illuminate\Contracts\Auth\UserProvider 实现只负责从持久化存储系统中获取 Illuminate\Contracts\Auth\Authenticatable 实现,例如 MySQL、Riak 等等。这两个接口允许 Laravel 认证机制继续起作用而不管用户数据如何存储或者何种类来展现。

让我们先看看 Illuminate\Contracts\Auth\UserProvider 契约:

<?php
    
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 函数通过唯一标识和存储在 remember_token 字段中的“记住我”令牌获取用户。和上一个方法一样,该方法也返回 Authenticatable 实现。

updateRememberToken 方法使用新的 $token 更新 $userremember_token 字段,新令牌可以是新生成的令牌(在登录是选择“记住我”被成功赋值)或者null(用户退出)。

retrieveByCredentials 方法在尝试登录系统时获取传递给 Auth::attempt 方法的认证信息数组。该方法接下来去底层持久化存储系统查询与认证信息匹配的用户,通常,该方法运行一个带“where”条件($credentials['username'])的查询。然后该方法返回 Authenticatable 的实现。这个方法不应该做任何密码校验和认证。

validateCredentials 方法比较给定 $user$credentials 来认证用户。例如,这个方法比较 $user->getAuthPassword() 字符串和经 Hash::check 处理的 $credentials['password']。这个方法根据密码是否有效返回布尔值 truefalse

Authenticatable 契约

既然我们已经探索了 UserProvider 上的每一个方法,接下来让我们看看 Authenticatable。记住,提供者需要从 retrieveByIdretrieveByTokenretrieveByCredentials 方法中返回接口实现:

<?php
    
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 中这将是自增 ID,getAuthPassword 返回经哈希处理的用户密码。

这个接口允许认证系统处理任何用户类,不管是你使用的是 ORM 还是存储抽象层。默认情况下,Laravel 在 app/Models 目录下提供的 App\Models\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\Validated' => [
        'App\Listeners\LogValidated',
    ],

    'Illuminate\Auth\Events\Verified' => [
        'App\Listeners\LogVerified',
    ],
    
    'Illuminate\Auth\Events\Logout' => [
        'App\Listeners\LogSuccessfulLogout',
    ],
    
    'Illuminate\Auth\Events\CurrentDeviceLogout' => [
        'App\Listeners\LogCurrentDeviceLogout',
    ],
    
    'Illuminate\Auth\Events\OtherDeviceLogout' => [
        'App\Listeners\LogOtherDeviceLogout',
    ],
    
    'Illuminate\Auth\Events\Lockout' => [
        'App\Listeners\LogLockout',
    ],
    
    'Illuminate\Auth\Events\PasswordReset' => [
        'App\Listeners\LogPasswordReset',
    ],
];

实例教程


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 日志

>> 下一篇: 用户授权