简介

大多数 Web 应用程序都为用户提供了重置遗忘密码的途径。Laravel 无需你为所创建的每个应用程序手动重新实现此功能,而是提供了便捷的服务,可用于发送密码重置链接以及安全地重置密码。

想快速上手?在全新的 Laravel 应用程序中安装 Laravel 应用程序入门套件。Laravel 的入门套件将负责搭建整个身份验证系统,包括重置遗忘密码功能。

配置

应用程序的密码重置配置文件位于 config/auth.php。请务必查看该文件中可供使用的选项。默认情况下,Laravel 配置为使用 database 密码重置驱动。

密码重置 driver 配置选项定义了密码重置数据的存储位置。Laravel 内置了两种驱动:

  • database - 密码重置数据存储在关系型数据库中。
  • cache - 密码重置数据存储在基于缓存的存储之一中。

驱动前置条件

数据库

当使用默认的 database 驱动时,必须创建一张表来存储应用程序的密码重置令牌。通常情况下,这张表包含在 Laravel 默认的 0001_01_01_000000_create_users_table.php 数据库迁移文件中。

缓存

还有一种可用于处理密码重置的缓存驱动,无需专门的数据库表。条目以用户邮箱地址的 SHA-256 哈希值作为键:

'passwords' => [
    'users' => [
        'driver' => 'cache',
        'provider' => 'users',
        'store' => 'passwords', // 可选...
        'expire' => 60,
        'throttle' => 60,
    ],
],

为防止调用 artisan cache:clear 时清空密码重置数据,你可以使用 store 配置键可选地指定单独的缓存存储。该值应与 config/cache.php 配置文件中配置的存储相对应。

模型准备

在使用 Laravel 的密码重置功能前,应用程序的 App\Models\User 模型必须使用 Illuminate\Notifications\Notifiable 特性。通常情况下,使用新的 Laravel 应用程序创建的默认 App\Models\User 模型中已包含此特性。

接下来,请确认你的 App\Models\User 模型实现了 Illuminate\Contracts\Auth\CanResetPassword 契约。框架自带的 App\Models\User 模型已实现该接口,并使用 Illuminate\Auth\Passwords\CanResetPassword 特性来包含实现该接口所需的方法。

配置可信主机

默认情况下,无论 HTTP 请求的 Host 标头内容如何,Laravel 都会响应其接收到的所有请求。此外,在 Web 请求期间生成指向应用程序的绝对 URL 时,会使用 Host 标头的值。

通常情况下,你应该配置 Web 服务器(如 Nginx 或 Apache),仅向应用程序发送符合指定主机名的请求。然而,若你无法直接自定义 Web 服务器,且需要指示 Laravel 仅响应特定主机名,则可在应用程序的 bootstrap/app.php 文件中使用 trustHosts 中间件方法来实现。当应用程序提供密码重置功能时,这一点尤为重要。

若想进一步了解此中间件方法,请参阅 TrustHosts 中间件文档。

路由

为妥善实现允许用户重置密码的支持,我们需要定义几个路由。首先,我们需要一组路由来处理用户通过邮箱地址请求密码重置链接的流程。其次,我们需要一组路由,用于在用户访问邮件中收到的密码重置链接并完成密码重置表单后,实际处理密码重置操作。

请求密码重置链接

密码重置链接请求表单

首先,我们将定义请求密码重置链接所需的路由。开始操作时,我们先定义一个返回密码重置链接请求表单视图的路由:

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

此路由返回的视图应包含一个带有 email 字段的表单,以便用户为指定邮箱地址请求密码重置链接。

处理表单提交

接下来,我们将定义一个处理来自“忘记密码”视图的表单提交请求的路由。此路由将负责验证邮箱地址,并将密码重置请求发送给相应用户:

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

Route::post('/forgot-password', function (Request $request) {
    $request->validate(['email' => 'required|email']);

    $status = Password::sendResetLink(
        $request->only('email')
    );

    return $status === Password::ResetLinkSent
        ? back()->with(['status' => __($status)])
        : back()->withErrors(['email' => __($status)]);
})->middleware('guest')->name('password.email');

继续之前,让我们更详细地了解此路由。首先,会对请求的 email 属性进行验证。接着,我们将通过 Laravel 内置的“密码代理”(借助 Password 门面)向用户发送密码重置链接。密码代理将负责按指定字段(本例中为邮箱地址)检索用户,并通过 Laravel 内置的通知系统向用户发送密码重置链接。

sendResetLink 方法会返回一个“状态”标识符。此状态可借助 Laravel 的本地化辅助函数进行翻译,以便向用户展示关于其请求状态的用户友好型提示信息。密码重置状态的翻译内容由应用程序的 lang/{lang}/passwords.php 语言文件决定。passwords 语言文件中包含了每个状态标识符可能取值的对应条目。

默认情况下,Laravel 应用程序骨架中不包含 lang 目录。若你想自定义 Laravel 的语言文件,可通过 lang:publish Artisan 命令发布这些文件。

你可能想知道,在调用 Password 门面的 sendResetLink 方法时,Laravel 是如何从应用程序的数据库中检索用户记录的。Laravel 的密码代理会借助身份验证系统中的“用户提供器”来检索数据库记录。密码代理所使用的用户提供器,是在 config/auth.php 配置文件的 passwords 配置数组中进行设置的。若想进一步了解如何编写自定义用户提供器,请参阅身份验证文档。

手动实现密码重置功能时,你需要自行定义视图和路由的内容。若你想要能搭建包含所有必要身份验证和验证逻辑的脚手架,可查看 Laravel 应用程序入门套件。

重置密码

密码重置表单

接下来,我们将定义用户点击邮件中收到的密码重置链接并提供新密码后,实际重置密码所需的路由。首先,我们来定义显示密码重置表单的路由,当用户点击密码重置链接时就会显示该表单。此路由会接收一个 token 参数,后续我们将用它来验证密码重置请求:

Route::get('/reset-password/{token}', function (string $token) {
    return view('auth.reset-password', ['token' => $token]);
})->middleware('guest')->name('password.reset');

此路由返回的视图应显示一个表单,其中包含 email 字段、password 字段、password_confirmation 字段,以及一个隐藏的 token 字段,该字段应包含路由接收到的密钥 $token 的值。

处理表单提交

当然,我们需要定义一个路由来实际处理密码重置表单的提交。此路由将负责验证传入的请求,并更新数据库中的用户密码:

use App\Models\User;
use Illuminate\Auth\Events\PasswordReset;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;

Route::post('/reset-password', function (Request $request) {
    $request->validate([
        'token' => 'required',
        'email' => 'required|email',
        'password' => 'required|min:8|confirmed',
    ]);

    $status = Password::reset(
        $request->only('email', 'password', 'password_confirmation', 'token'),
        function (User $user, string $password) {
            $user->forceFill([
                'password' => Hash::make($password)
            ])->setRememberToken(Str::random(60));

            $user->save();

            event(new PasswordReset($user));
        }
    );

    return $status === Password::PasswordReset
        ? redirect()->route('login')->with('status', __($status))
        : back()->withErrors(['email' => [__($status)]]);
})->middleware('guest')->name('password.update');

在继续之前,让我们更详细地研究这个路由。首先,请求的 token、email 和 password 属性会被验证。接下来,我们将使用 Laravel 内置的“密码代理”(通过 Password 门面)来验证密码重置请求的凭证。

如果提供给密码代理的令牌、邮箱地址和密码均有效,传入 reset 方法的闭包将被调用。在这个闭包中,它接收用户实例以及提供给密码重置表单的明文密码,我们可以在数据库中更新用户的密码。

reset 方法会返回一个“状态”标识符。可以使用 Laravel 的本地化辅助函数对该状态进行翻译,以便向用户显示关于其请求状态的友好提示。密码重置状态的翻译由应用程序的 lang/{lang}/passwords.php 语言文件决定。在 passwords 语言文件中,包含了状态标识符每个可能值的对应条目。如果应用程序中没有 lang 目录,可以使用 lang:publish Artisan 命令创建它。

在继续之前,您可能会想知道,当调用 Password 门面的 reset 方法时,Laravel 是如何知道从应用程序的数据库中检索用户记录的。Laravel 密码代理利用身份验证系统的“用户提供程序”来检索数据库记录。密码代理所使用的用户提供程序在 config/auth.php 配置文件的 passwords 配置数组中进行配置。若要了解有关编写自定义用户提供程序的更多信息,请参阅身份验证文档。

删除过期令牌

如果您使用的是 database 驱动,过期的密码重置令牌仍将保留在数据库中。不过,您可以使用 auth:clear-resets Artisan 命令轻松删除这些记录:

php artisan auth:clear-resets

如果您希望将此过程自动化,可以考虑将该命令添加到应用程序的调度器中:

use Illuminate\Support\Facades\Schedule;

Schedule::command('auth:clear-resets')->everyFifteenMinutes();

自定义

重置链接自定义

您可以使用 ResetPassword 通知类提供的 createUrlUsing 方法,自定义密码重置链接的 URL。该方法接受一个闭包,该闭包会接收到接收通知的用户实例以及密码重置链接的令牌。通常情况下,应在应用程序的 AppServiceProvider 的 boot 方法中调用该方法:

use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;

/**
 * Bootstrap any application services.
 */
public function boot(): void
{
    ResetPassword::createUrlUsing(function (User $user, string $token) {
        return 'https://example.com/reset-password?token='.$token;
    });
}

重置邮件自定义

您可以轻松修改用于向用户发送密码重置链接的通知类。要开始操作,请在 App\Models\User 模型中重写 sendPasswordResetNotification 方法。在该方法中,您可以使用自行创建的任何通知类来发送通知。密码重置的 $token 是该方法接收到的第一个参数。您可以使用该 $token 来构建所需的密码重置 URL,并向用户发送通知:

use App\Notifications\ResetPasswordNotification;

/**
 * Send a password reset notification to the user.
 *
 * @param  string  $token
 */
public function sendPasswordResetNotification($token): void
{
    $url = 'https://example.com/reset-password?token='.$token;

    $this->notify(new ResetPasswordNotification($url));
}