认证

介绍

许多 Web 应用程序为用户提供了一种身份验证和 “登录” 的方式。在 Web 应用程序中实现此功能可能是一个复杂且具有潜在风险的任务。正因如此,Laravel 力求为您提供快速、安全、简便的身份验证工具。

从本质上讲,Laravel 的身份验证功能由 “守卫”(guards)和 “提供者”(providers)构成。守卫定义了每个请求如何进行用户身份验证。例如,Laravel 默认提供了一个会话守卫,它通过会话存储和 cookies 来维护状态。

提供者定义了如何从持久化存储中检索用户。Laravel 默认支持使用 Eloquent 和数据库查询构建器来检索用户。不过,您可以根据需要为应用程序定义额外的提供者。

您的应用程序的身份验证配置文件位于 config/auth.php。该文件包含多个文档清晰的选项,用于调整 Laravel 身份验证服务的行为。

守卫和提供者不应与 “角色” 和 “权限” 混淆。要了解如何通过权限授权用户操作,请参阅【授权文档】。

启动套件

想要快速上手吗?在一个全新的 Laravel 应用程序中安装一个 Laravel 应用程序启动套件。迁移数据库后,打开浏览器访问 /register 或任何其它分配给您应用程序的 URL。启动套件将自动生成整个身份验证系统的脚手架!

即使您选择在最终的 Laravel 应用程序中不使用启动套件,安装 【Laravel Breeze】 启动套件也是一个很好的机会,可以学习如何在实际的 Laravel 项目中实现 Laravel 所有的身份验证功能。由于 Laravel Breeze 会为您创建身份验证控制器、路由和视图,您可以查看这些文件中的代码,了解如何实现 Laravel 的身份验证功能。

数据库考虑

默认情况下,Laravel 在您的 app/Models 目录中包含一个 App\Models\User Eloquent 模型。此模型可与默认的 Eloquent 身份验证驱动程序一起使用。

如果您的应用程序没有使用 Eloquent,您可以使用数据库身份验证提供程序,该提供程序使用 Laravel 的查询构建器。如果您的应用程序使用 MongoDB,可以参考 MongoDB 的官方 【Laravel 用户身份验证文档】。

在为 App\Models\User 模型构建数据库模式时,确保 password 列至少为 60 个字符长。当然,新 Laravel 应用程序中包含的 users 表迁移已经创建了一个长度超过此限制的列。

此外,您应该确保您的 users(或等效)表中包含一个可空的、长度为 100 字符的 remember_token 字符串列。此列将用于存储在用户登录时选择 "记住我" 选项的用户的令牌。同样,新 Laravel 应用程序中包含的默认 users 表迁移已经包含此列。

生态系统概述

Laravel 提供了几个与身份验证相关的包。在继续之前,我们将回顾 Laravel 的身份验证生态系统,并讨论每个包的用途。

首先,考虑一下身份验证是如何工作的。当使用 Web 浏览器时,用户通过登录表单提供他们的用户名和密码。如果这些凭据正确,应用程序将把经过身份验证的用户信息存储在用户的【会话】中。发给浏览器的 cookie 包含会话 ID,以便随后的请求能够将用户与正确的会话关联起来。在接收到会话 cookie 后,应用程序将根据会话 ID 获取会话数据,注意到身份验证信息已经存储在会话中,并将用户视为“已认证”。

当远程服务需要进行身份验证以访问 API 时,通常不会使用 cookie 进行身份验证,因为没有 Web 浏览器。相反,远程服务会在每个请求中向 API 发送一个 API 令牌。应用程序可以通过与有效的 API 令牌表进行验证来“认证”该请求,证明该请求是由与该 API 令牌关联的用户执行的。

Laravel 内置的浏览器身份验证服务

Laravel 包含内置的身份验证和会话服务,通常通过 AuthSession 门面访问。这些功能为通过 Web 浏览器发起的请求提供基于 cookie 的身份验证。它们提供了验证用户凭据和进行身份验证的方法。此外,这些服务会自动将适当的身份验证数据存储在用户的会话中,并发出用户的会话 cookie。有关如何使用这些服务的详细信息,请参见本文档。

应用程序启动器

如本文档所述,您可以手动与这些身份验证服务交互,以构建您应用程序的身份验证层。然而,为了帮助您更快入门,我们发布了 免费的包,这些包为整个身份验证层提供了强大的现代化脚手架。这些包包括 Laravel BreezeLaravel JetstreamLaravel Fortify

  • Laravel Breeze 是所有 Laravel 身份验证功能的简单、最小实现,包括登录、注册、密码重置、电子邮件验证和密码确认。Laravel Breeze 的视图层由简单的 【Blade 模板】组成,并使用 【Tailwind CSS】 进行样式设计。要入门,请查看 Laravel 【应用程序启动器】的文档。

  • Laravel Fortify 是一个无头的身份验证后端,它实现了本教程中介绍的许多功能,包括基于 cookie 的身份验证以及其它功能,如两步验证和电子邮件验证。Fortify 为 Laravel Jetstream 提供身份验证后端,也可以与 【Laravel Sanctum】 配合使用,为需要与 Laravel 进行身份验证的单页应用程序(SPA)提供身份验证服务。

  • Laravel Jetstream 是一个强大的应用程序启动器,它通过一个美观、现代化的 UI(由 Tailwind CSSLivewire 和/或 Inertia 提供支持)来消费并公开 Laravel Fortify 的身份验证服务。Laravel Jetstream 包括可选的两步验证支持、团队支持、浏览器会话管理、个人资料管理,并与 Laravel Sanctum 内置集成,提供 API 令牌身份验证。Laravel 的 API 身份验证功能将在下文中讨论。

Laravel 的 API 身份验证服务

Laravel 提供了两个可选的包来帮助您管理 API 令牌并对使用 API 令牌的请求进行身份验证:【Passport】 和 【Sanctum】。请注意,这些库与 Laravel 内置的基于 cookie 的身份验证库并不互相排斥。这些库主要关注 API 令牌身份验证,而内置的身份验证服务则专注于基于浏览器的 cookie 身份验证。许多应用程序将同时使用 Laravel 的内置基于 cookie 的身份验证服务和其中一个 Laravel 的 API 身份验证包。

Passport

Passport 是一个 OAuth2 身份验证提供程序,提供多种 OAuth2 "授权类型",允许您颁发各种类型的令牌。通常,这是一个为 API 身份验证提供的强大而复杂的包。然而,大多数应用程序并不需要 OAuth2 规范所提供的复杂功能,这些功能可能会让用户和开发者感到困惑。此外,开发者们历来对如何使用像 Passport 这样的 OAuth2 身份验证提供程序来对 SPA 应用或移动应用进行身份验证感到困惑。

Sanctum

Sanctum 是针对 OAuth2 复杂性和开发者困惑的一种回应,我们构建了一个更简单、更加精简的身份验证包,它可以同时处理来自 Web 浏览器的第一方 Web 请求和通过令牌进行的 API 请求。这个目标通过发布 Laravel Sanctum 得以实现,它应该被视为为那些提供第一方 Web UI 和 API 的应用程序、或者是由与后端 Laravel 应用程序分离的单页应用程序(SPA)驱动的应用程序、或提供移动客户端的应用程序提供身份验证的首选和推荐包。

Laravel Sanctum 是一个混合 Web / API 身份验证包,可以管理您应用程序的整个身份验证过程。其实现方式是,当 Sanctum 基础的应用程序收到请求时,Sanctum 会首先检查请求是否包含引用经过身份验证的会话的会话 cookie。Sanctum 通过调用 Laravel 内置的身份验证服务来完成这一过程。如果请求没有通过会话 cookie 进行身份验证,Sanctum 将检查请求是否包含 API 令牌。如果请求中包含 API 令牌,Sanctum 将使用该令牌对请求进行身份验证。要了解更多关于此过程的信息,请参考 Sanctum 的 【“如何工作”】 文档。

Laravel Sanctum 是我们为 【Laravel Jetstream】 应用程序启动器选择的 API 包,因为我们认为它最适合大多数 Web 应用程序的身份验证需求。

总结与选择您的技术栈

总之,如果您的应用程序将通过浏览器访问,并且您正在构建一个单体的 Laravel 应用程序,那么您的应用程序将使用 Laravel 的内置身份验证服务。

接下来,如果您的应用程序提供了一个将由第三方使用的 API,您将选择 Passport 或 Sanctum 来为应用程序提供 API 令牌身份验证。通常情况下,应该优先选择 Sanctum,因为它是一个简单、完整的 API 身份验证、SPA 身份验证和移动身份验证解决方案,包括对 “作用域” 或 “能力” 的支持。

如果您正在构建一个由 Laravel 后端驱动的单页应用程序(SPA),应该使用 【Laravel Sanctum】。在使用 Sanctum 时,您将需要【手动实现自己的后端身份验证路由】,或利用 【Laravel Fortify】 作为一个无头身份验证后端服务,提供路由和控制器来支持注册、密码重置、电子邮件验证等功能。

当您的应用程序确实需要 OAuth2 规范提供的所有功能时,可以选择使用 Passport。

如果您想快速入门,我们非常推荐使用 【Laravel Breeze】,它可以作为启动新的 Laravel 应用程序的快速方法,已经使用了我们推荐的身份验证技术栈:Laravel 的内置身份验证服务和 Laravel Sanctum。

身份验证快速入门

这部分文档讨论了通过 【Laravel 应用程序启动器】进行用户身份验证的内容,其中包括帮助您快速入门的 UI 脚手架。如果您希望直接集成 Laravel 的身份验证系统,请查看关于【手动认证用户】的文档。

安装启动套件

首先,您应该【安装一个 Laravel 应用程序启动器包】。我们当前的启动器包,Laravel Breeze 和 Laravel Jetstream,为您将身份验证功能集成到全新的 Laravel 应用程序中提供了精美的起点。

Laravel Breeze 是一个简约的实现,包含了 Laravel 所有身份验证功能,包括登录、注册、密码重置、电子邮件验证和密码确认。Laravel Breeze 的视图层由简单的 【Blade 模板】构成,并使用 【Tailwind CSS】 进行样式化。此外,Breeze 提供了基于 【Livewire】 或 【Inertia】 的脚手架选项,您可以选择在 Inertia 基础的脚手架中使用 Vue 或 React。

【Laravel Jetstream】 是一个更强大的应用程序启动器包,支持使用 【Livewire】 或 【Inertia 和 Vue】 来搭建您的应用程序。此外,Jetstream 还提供可选的两因素身份验证、团队管理、个人资料管理、浏览器会话管理、通过 【Laravel Sanctum】 提供的 API 支持、账户删除等功能。

检索已认证的用户

在安装完身份验证启动器包并允许用户注册和认证后,您通常需要与当前认证的用户进行交互。在处理传入请求时,您可以通过 Auth facade 的 user 方法来访问当前认证的用户:

use Illuminate\Support\Facades\Auth;

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

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

或者,一旦用户通过身份验证,您也可以通过 Illuminate\Http\Request 实例访问当前认证的用户。请记住,带类型提示的类会自动注入到您的控制器方法中。通过对 Illuminate\Http\Request 对象进行类型提示,您可以方便地通过请求的 user 方法在应用程序中的任何控制器方法中访问认证的用户:

<?php

namespace App\Http\Controllers;

use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;

class FlightController extends Controller
{
    /**
     * 更新现有航班的信息。
     */
    public function update(Request $request): RedirectResponse
    {
        $user = $request->user();

        // ...

        return redirect('/flights');
    }
}

判断当前用户是否已认证

要判断发起传入 HTTP 请求的用户是否已认证,您可以使用 Auth facade 上的 check 方法。如果用户已认证,该方法将返回 true

use Illuminate\Support\Facades\Auth;

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

虽然可以通过 check 方法判断用户是否已认证,但通常您会使用中间件在允许用户访问某些路由/控制器之前验证用户是否已认证。有关详细信息,请查看有关【保护路由】的文档。

保护路由

【路由中间件】可用于仅允许已认证的用户访问某个路由。Laravel 自带了一个 auth 中间件,它是 Illuminate\Auth\Middleware\Authenticate 类的【中间件别名】。由于 Laravel 已经在内部为其创建了别名,您只需要将中间件附加到路由定义中:

Route::get('/flights', function () {
    // 只有已认证的用户才能访问此路由...
})->middleware('auth');

重定向未认证的用户

auth 中间件检测到用户未认证时,它会将用户重定向到登录页面(即登录的【命名路由】)。您可以通过修改应用程序的 bootstrap/app.php 文件中的 redirectGuestsTo 方法来更改此行为:

use Illuminate\Http\Request;

->withMiddleware(function (Middleware $middleware) {
    $middleware->redirectGuestsTo('/login');

    // 使用闭包...
    $middleware->redirectGuestsTo(fn (Request $request) => route('login'));
})

指定一个 Guard

在将 auth 中间件附加到路由时,您还可以指定应使用哪个 “守卫” 来认证用户。指定的守卫应与 auth.php 配置文件中 guards 数组中的键之一对应:

Route::get('/flights', function () {
    // 只有已认证的用户才能访问此路由...
})->middleware('auth:admin');

登录限流

如果您使用的是 Laravel Breeze 或 Laravel Jetstream 【启动包】,则登录尝试会自动应用速率限制。默认情况下,如果用户在多次尝试后未能提供正确的凭据,用户将在一分钟内无法登录。限流是基于用户的用户名/电子邮件地址和 IP 地址的。

如果您希望为应用程序中的其它路由应用速率限制,请查看【速率限制文档】。

手动认证用户

您不需要使用 Laravel 【应用程序启动包】中包含的身份验证脚手架。如果您选择不使用这些脚手架,您将需要直接使用 Laravel 身份验证类来管理用户身份验证。别担心,这非常简单!

我们将通过 Auth 【facade】 访问 Laravel 的身份验证服务,因此需要确保在类的顶部导入 Auth facade。接下来,我们来看看 attempt 方法。attempt 方法通常用于处理应用程序 “登录” 表单中的身份验证尝试。如果身份验证成功,您应该重新生成用户的会话,以防止会话固定攻击:

<?php

namespace App\Http\Controllers;

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

class LoginController extends Controller
{
    /**
     * 处理身份验证尝试。
     */
    public function authenticate(Request $request): RedirectResponse
    {
        $credentials = $request->validate([
            'email' => ['required', 'email'],
            'password' => ['required'],
        ]);

        if (Auth::attempt($credentials)) {
            $request->session()->regenerate();

            return redirect()->intended('dashboard');
        }

        return back()->withErrors([
            'email' => '提供的凭据与我们的记录不匹配。',
        ])->onlyInput('email');
    }
}

attempt 方法接受一个包含键值对的数组作为第一个参数。该数组中的值将用于在您的数据库表中查找用户。例如,上面的例子中,将通过 email 字段的值来检索用户。如果找到了用户,存储在数据库中的哈希密码将与传递给该方法的密码值进行比较。在框架自动哈希值并将其与数据库中的哈希密码进行比较之前,您不应对传入的请求的密码值进行哈希处理。如果两个哈希密码匹配,将为该用户启动一个认证会话。

请记住,Laravel 的身份验证服务将根据您认证守卫(guard)配置中的“provider” 配置从数据库中检索用户。在默认的 config/auth.php 配置文件中,指定了 Eloquent 用户提供程序,并指示它在检索用户时使用 App\Models\User 模型。您可以根据应用程序的需求在配置文件中更改这些值。

attempt 方法会在身份验证成功时返回 true,否则返回 false

Laravel 的重定向器提供的 intended 方法将把用户重定向到他们在被身份验证中间件拦截之前试图访问的 URL。如果目标 URL 不可用,可以为此方法提供一个后备 URI。

指定附加条件

如果需要,您还可以在用户的电子邮件和密码之外,向身份验证查询添加额外的查询条件。为此,您只需将查询条件添加到传递给 attempt 方法的数组中。例如,我们可以验证用户是否标记为 “激活” 状态:

if (Auth::attempt(['email' => $email, 'password' => $password, 'active' => 1])) {
    // 身份验证成功...
}

对于复杂的查询条件,您可以在凭据数组中提供一个闭包。这个闭包将接收查询实例,允许您根据应用程序的需要自定义查询:

use Illuminate\Database\Eloquent\Builder;

if (Auth::attempt([
    'email' => $email,
    'password' => $password,
    fn (Builder $query) => $query->has('activeSubscription'),
])) {
    // 身份验证成功...
}

在这些例子中,email 不是必须的选项,它仅作为示例。您应该使用与数据库表中 “用户名” 对应的列名。

attemptWhen 方法接受一个闭包作为第二个参数,可以在实际认证用户之前执行更广泛的检查。闭包接收潜在的用户并返回 truefalse,以指示是否可以认证该用户:

if (Auth::attemptWhen([
    'email' => $email,
    'password' => $password,
], function (User $user) {
    return $user->isNotBanned();
})) {
    // 身份验证成功...
}

访问特定的 Guard 实例

通过 Auth facade 的 guard 方法,您可以指定在认证用户时要使用的守卫实例。这使得您可以为应用程序的不同部分管理使用完全独立的可认证模型或用户表的身份验证。

传递给 guard 方法的守卫名称应与您 auth.php 配置文件中的 guards 数组中的键之一对应:

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

记住用户

许多 Web 应用程序在登录表单中提供一个“记住我”复选框。如果您希望在应用程序中提供“记住我”功能,可以将一个布尔值作为第二个参数传递给 attempt 方法。

当该值为 true 时,Laravel 会保持用户的登录状态,直到他们手动注销或超过会话有效期。您的 users 表必须包含一个 remember_token 字符串列,该列用于存储“记住我”令牌。新创建的 Laravel 应用程序的 users 表迁移已经包括了此列:

use Illuminate\Support\Facades\Auth;

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

如果您的应用程序提供 “记住我” 功能,您可以使用 viaRemember 方法来判断当前认证的用户是否是通过 “记住我” cookie 进行身份验证的:

use Illuminate\Support\Facades\Auth;

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

其它认证方法

身份验证用户实例

如果您需要将现有的用户实例设置为当前认证的用户,可以将用户实例传递给 Auth facade 的 login 方法。给定的用户实例必须实现 Illuminate\Contracts\Auth\Authenticatable 合同。Laravel 中包含的 App\Models\User 模型已经实现了这个接口。当您已经有一个有效的用户实例时,这种认证方式很有用,例如在用户成功注册后:

use Illuminate\Support\Facades\Auth;

Auth::login($user);

您可以将一个布尔值作为第二个参数传递给 login 方法。这个值表示是否希望启用 “记住我” 功能。这意味着会话将会被永久认证,直到用户手动退出应用程序:

Auth::login($user, $remember = true);

如果需要,您可以在调用 login 方法之前指定一个认证守卫:

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

通过 ID 认证用户

要通过用户数据库记录的主键来认证用户,您可以使用 loginUsingId 方法。该方法接受您希望认证的用户的主键:

Auth::loginUsingId(1);

您可以将一个布尔值传递给 loginUsingId 方法的 remember 参数。这个值指示是否希望在认证会话中启用 “记住我” 功能。请记住,这意味着会话将被认证为无限期有效,直到用户手动从应用程序中登出为止。

Auth::loginUsingId(1, remember: true);

临时认证用户

您可以使用 once 方法在单次请求中认证用户。调用此方法时不会使用会话或 cookies:

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

HTTP 基本认证

【HTTP 基本认证】提供了一种快速的方式来认证您的应用程序用户,而无需设置专门的 “登录” 页面。要开始使用,只需将 auth.basic 中间件附加到某个路由。auth.basic 中间件已包含在 Laravel 框架中,因此无需自定义:

Route::get('/profile', function () {
    // 只有已认证的用户可以访问此路由...
})->middleware('auth.basic');

将中间件附加到路由后,您在浏览器访问该路由时会自动被要求提供凭据。默认情况下,auth.basic 中间件将假定用户数据库表中的 email 列是用户的 “用户名”。

关于 FastCGI 的说明

如果您使用的是 PHP FastCGI 和 Apache 来提供 Laravel 应用程序,HTTP 基本认证可能无法正确工作。为了解决这些问题,可以在应用程序的 .htaccess 文件中添加以下行:

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

无状态 HTTP 基本认证

您还可以在不设置用户标识符会话 cookie 的情况下使用 HTTP 基本认证。这对于选择使用 HTTP 认证来验证应用程序 API 请求的情况特别有用。要实现这一点,您需要定义一个中间件,该中间件调用 onceBasic 方法。如果 onceBasic 方法没有返回响应,请求可以继续传递到应用程序的其它部分:

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
use Symfony\Component\HttpFoundation\Response;

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

接下来,将中间件附加到某个路由:

Route::get('/api/user', function () {
    // 只有已认证的用户可以访问此路由...
})->middleware(AuthenticateOnceWithBasicAuth::class);

注销

要手动将用户登出您的应用程序,您可以使用 Auth facade 提供的 logout 方法。此方法会从用户的会话中移除认证信息,以便后续请求不再认证。

除了调用 logout 方法外,建议您使用户的会话失效并重新生成 【CSRF 令牌】。登出用户后,通常会将用户重定向到应用程序的根目录:

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

/**
 * 将用户登出应用程序。
 */
public function logout(Request $request): RedirectResponse
{
    Auth::logout();

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

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

    return redirect('/');
}

在其它设备上使会话失效

Laravel 还提供了一种机制,用于使用户在其它设备上的活动会话失效,而不会使当前设备上的会话失效。此功能通常在用户更改或更新密码时使用,以便在保持当前设备认证的同时,使其它设备上的会话失效。

在开始之前,您应该确保在需要会话认证的路由上包含 Illuminate\Session\Middleware\AuthenticateSession 中间件。通常,您应该将此中间件放在路由组定义中,以便将其应用到应用程序的大部分路由。默认情况下,AuthenticateSession 中间件可以通过 auth.session 中间件别名附加到路由:

Route::middleware(['auth', 'auth.session'])->group(function () {
    Route::get('/', function () {
        // ...
    });
});

然后,您可以使用 Auth facade 提供的 logoutOtherDevices 方法。此方法要求用户确认其当前密码,您的应用程序应该通过输入表单接受此密码:

use Illuminate\Support\Facades\Auth;

Auth::logoutOtherDevices($currentPassword);

当调用 logoutOtherDevices 方法时,用户在其它设备上的会话将完全失效,这意味着他们将在所有曾经认证过的守卫中被 “登出”。

密码确认

在构建应用程序时,您可能会遇到一些操作,在执行这些操作之前,用户需要确认其密码,或者在用户访问应用程序的敏感区域之前,需要进行密码确认。Laravel 提供了内置的中间件来简化这个过程。实现此功能需要您定义两个路由:一个路由用于显示要求用户确认密码的视图,另一个路由用于确认密码是否有效,并将用户重定向到他们的目标位置。

以下文档讨论了如何直接集成 Laravel 的密码确认功能;但是,如果您想更快地入门,Laravel 应用程序启动包已经支持此功能!

配置

在确认密码后,用户将在三小时内无需再次确认密码。不过,您可以通过更改应用程序的 config/auth.php 配置文件中的 password_timeout 配置值,来配置用户再次被提示输入密码的时间长度。

路由

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

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

如您所料,该路由返回的视图应包含一个密码字段的表单。此外,您可以在视图中添加文本,解释用户正在进入应用程序的受保护区域,并必须确认他们的密码。

确认密码 接下来,我们定义一个路由来处理“确认密码”视图中的表单请求。该路由将负责验证密码并将用户重定向到他们的目标位置:

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']);

在继续之前,让我们更详细地检查这个路由。首先,验证请求中的密码字段是否与已认证用户的密码匹配。如果密码有效,我们需要通知 Laravel 的会话用户已确认其密码。passwordConfirmed 方法将设置一个时间戳,Laravel 可以使用它来判断用户最后一次确认密码的时间。最后,我们可以将用户重定向到他们的目标位置。

保护路由

您应该确保任何需要近期密码确认的操作都分配了 password.confirm 中间件。此中间件是 Laravel 默认安装的一部分,它会自动将用户的目标位置存储在会话中,以便用户在确认密码后可以被重定向到该位置。中间件在将用户的目标位置存储在会话后,会将用户重定向到 password.confirm 命名路由:

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

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

添加自定义守卫

您可以使用 Auth facade 的 extend 方法定义自己的身份验证守卫。应该在服务提供者中调用 extend 方法。由于 Laravel 已经包含了 AppServiceProvider,我们可以将代码放入该提供者中:

<?php

namespace App\Providers;

use App\Services\Auth\JwtGuard;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    // ...

    /**
     * 启动任何应用程序服务。
     */
    public function boot(): void
    {
        Auth::extend('jwt', function (Application $app, string $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 方法。这个方法允许您通过一个闭包快速定义身份验证过程。

要开始,您需要在应用程序的 AppServiceProviderboot 方法中调用 Auth::viaRequest 方法。viaRequest 方法接受一个身份验证驱动名称作为第一个参数。这个名称可以是任何描述您自定义守卫的字符串。方法的第二个参数应该是一个闭包,该闭包接收传入的 HTTP 请求并返回一个用户实例,或者如果身份验证失败,返回 null

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

/**
 * 启动任何应用程序服务。
 */
public function boot(): void
{
    Auth::viaRequest('custom-token', function (Request $request) {
        return User::where('token', (string) $request->token)->first();
    });
}

一旦定义了自定义身份验证驱动,您可以将其配置为 auth.php 配置文件中的守卫驱动:

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

最后,您可以在将身份验证中间件分配给路由时引用该守卫:

Route::middleware('auth:api')->group(function () {
    // ...
});

添加自定义用户提供者

如果您没有使用传统的关系型数据库来存储用户数据,您将需要通过自定义身份验证用户提供者来扩展 Laravel。我们将使用 Auth facade 上的 provider 方法来定义一个自定义用户提供者。该用户提供者解析器应返回 Illuminate\Contracts\Auth\UserProvider 的实现。

以下是如何实现的示例:

namespace App\Providers;

use App\Extensions\MongoUserProvider;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\ServiceProvider;

class AppServiceProvider extends ServiceProvider
{
    // ...

    /**
     * 启动任何应用服务。
     */
    public function boot(): void
    {
        Auth::provider('mongo', function (Application $app, array $config) {
            // 返回一个 Illuminate\Contracts\Auth\UserProvider 的实例...

            return new MongoUserProvider($app->make('mongo.connection'));
        });
    }
}

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

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

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

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

用户提供者契约

Illuminate\Contracts\Auth\UserProvider 的实现负责从持久化存储系统(如 MySQL、MongoDB 等)中获取 Illuminate\Contracts\Auth\Authenticatable 的实现。这两个接口允许 Laravel 身份验证机制继续工作,而不管用户数据是如何存储的,或者用于表示已认证用户的类是什么。

让我们看一下 Illuminate\Contracts\Auth\UserProvider 合同:

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);
    public function rehashPasswordIfRequired(Authenticatable $user, array $credentials, bool $force = false);
}
  • retrieveById:该方法通常接收一个表示用户的标识符,比如来自 MySQL 数据库的自动递增 ID。此方法应返回与该 ID 匹配的 Authenticatable 实现。

  • retrieveByToken:该方法通过其唯一的 $identifier 和 "remember me" $token(通常存储在像 remember_token 这样的数据库列中)来检索用户。与前述方法一样,方法应该返回具有匹配 token 值的 Authenticatable 实现。

  • updateRememberToken:该方法更新 $user 实例的 remember_token 为新 $token。在成功的 "remember me" 身份验证尝试或用户注销时,会为用户分配一个新的 token。

  • retrieveByCredentials:该方法接收通过 Auth::attempt 方法传递的凭证数组,当尝试在应用中进行身份验证时使用。该方法应该查询底层的持久化存储,找到与这些凭证匹配的用户。通常,该方法会执行一个 "where" 条件查询,搜索与 $credentials['username'] 匹配的 "用户名" 的用户记录。此方法应返回一个 Authenticatable 实现。此方法不应尝试进行密码验证或身份验证。

  • validateCredentials:该方法应比较给定的 $user$credentials 来进行身份验证。例如,该方法通常会使用 Hash::check 方法来比较 $user→getAuthPassword() 的值与 $credentials['password'] 的值。该方法应返回 truefalse,表示密码是否有效。

  • rehashPasswordIfRequired:该方法应该在需要并且支持的情况下重新哈希给定 $user 的密码。例如,该方法通常会使用 Hash::needsRehash 方法来检查 $credentials['password'] 是否需要重新哈希。如果密码需要重新哈希,该方法应使用 Hash::make 方法重新哈希密码,并更新用户在底层持久化存储中的记录。

可认证契约

现在我们已经详细了解了 UserProvider 上的每个方法,接下来让我们看看 Authenticatable 合同。请记住,用户提供者应从 retrieveByIdretrieveByTokenretrieveByCredentials 方法中返回该接口的实现:

namespace Illuminate\Contracts\Auth;

interface Authenticatable
{
    public function getAuthIdentifierName();
    public function getAuthIdentifier();
    public function getAuthPasswordName();
    public function getAuthPassword();
    public function getRememberToken();
    public function setRememberToken($value);
    public function getRememberTokenName();
}

这个接口非常简单。 - getAuthIdentifierName 方法应返回用户的 "主键" 列的名称。 - getAuthIdentifier 方法应返回用户的 "主键"。当使用 MySQL 后端时,这通常是分配给用户记录的自动递增主键。 - getAuthPasswordName 方法应返回用户的密码列的名称。 - getAuthPassword 方法应返回用户的哈希密码。

这个接口使得身份验证系统能够与任何 "用户" 类一起使用,无论你使用什么 ORM 或存储抽象层。 默认情况下,Laravel 在 app/Models 目录中包含了一个实现该接口的 App\Models\User 类。

自动密码重哈希

Laravel 默认的密码哈希算法是 bcrypt。你可以通过应用程序的 config/hashing.php 配置文件或 BCRYPT_ROUNDS 环境变量来调整 bcrypt 哈希的 "工作因子"。

通常,随着 CPU / GPU 处理能力的增加,bcrypt 的工作因子应该逐步提高。如果你提高应用程序的 bcrypt 工作因子,Laravel 会在用户通过 Laravel 的启动套件进行身份验证时,或当你通过 attempt 方法手动验证用户时,自动平稳地重新哈希用户的密码。

通常,自动密码重新哈希的行为不会对应用程序造成干扰;然而,你可以通过发布哈希配置文件来禁用此行为:

php artisan config:publish hashing

发布配置文件后,你可以将 rehash_on_login 配置值设置为 false

'rehash_on_login' => false,

事件

Laravel 在身份验证过程中会触发多种事件。你可以为以下事件定义监听器:

事件名称:

  • Illuminate\Auth\Events\Registered

  • Illuminate\Auth\Events\Attempting

  • Illuminate\Auth\Events\Authenticated

  • Illuminate\Auth\Events\Login

  • Illuminate\Auth\Events\Failed

  • Illuminate\Auth\Events\Validated

  • Illuminate\Auth\Events\Verified

  • Illuminate\Auth\Events\Logout

  • Illuminate\Auth\Events\CurrentDeviceLogout

  • Illuminate\Auth\Events\OtherDeviceLogout

  • Illuminate\Auth\Events\Lockout

  • Illuminate\Auth\Events\PasswordReset