重置密码

介绍

大多数 web 应用程序都提供了让用户重置忘记密码的功能。为了避免每次创建应用程序时都需要重新手动实现这一功能,Laravel 提供了方便的服务来发送密码重置链接和安全地重置密码。

想快速入门吗?在一个新的 Laravel 应用程序中安装 【Laravel 应用程序启动包】。Laravel 的启动包会为您搭建完整的认证系统,包括重置忘记的密码。

模型准备

在使用 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 特性来包含实现该接口所需的方法。

数据库准备

必须创建一个表来存储应用程序的密码重置令牌。通常,这在 Laravel 默认的 0001_01_01_000000_create_users_table.php 数据库迁移文件中已经包含。

配置受信任的主机

默认情况下,Laravel 会响应它接收到的所有请求,无论 HTTP 请求的 Host 头部的内容如何。此外,在生成应用程序的绝对 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');

这个路由返回的视图应该包含一个表单,表单中有一个电子邮件字段,允许用户为指定的电子邮件地址请求密码重置链接。

处理表单提交

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

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::RESET_LINK_SENT
                ? back()->with(['status' => __($status)])
                : back()->withErrors(['email' => __($status)]);
})->middleware('guest')->name('password.email');

在继续之前,我们先更详细地了解这个路由。首先,验证请求中的 email 属性。接下来,我们将使用 Laravel 内置的 “密码代理” (通过 Password 门面)来向用户发送密码重置链接。密码代理会根据给定的字段(在本例中是电子邮件地址)来检索用户,并通过 Laravel 内置的【通知系统】向用户发送密码重置链接。

sendResetLink 方法返回一个 “状态” 标记。该状态可以通过 Laravel 的本地化助手进行翻译,以便向用户显示有关请求状态的用户友好消息。密码重置状态的翻译由您应用程序中的 lang/{lang}/passwords.php 语言文件决定。每个可能的状态值条目都位于该语言文件中。

默认情况下,Laravel 应用程序骨架不包含 lang 目录。如果您希望自定义 Laravel 的语言文件,可以通过 lang:publish Artisan 命令发布它们。

您可能会想知道 Laravel 是如何在调用 Password 门面的 sendResetLink 方法时知道如何从应用程序的数据库中检索用户记录的。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::PASSWORD_RESET
                ? redirect()->route('login')->with('status', __($status))
                : back()->withErrors(['email' => [__($status)]]);
})->middleware('guest')->name('password.update');

在继续之前,我们先更详细地了解这个路由。首先,验证请求中的 tokenemailpassword 属性。接下来,我们将使用 Laravel 内置的“密码代理” (通过 Password 门面)来验证密码重置请求的凭据。

如果密码代理验证通过(即 tokenemailpassword 是有效的),则会调用传递给 reset 方法的闭包。在这个闭包中,我们接收 user 实例和密码重置表单中提供的明文密码,我们可以在数据库中更新用户的密码。

reset 方法返回一个 “状态” 标记。该状态可以通过 Laravel 的本地化助手进行翻译,以便向用户显示有关请求状态的用户友好消息。密码重置状态的翻译由您应用程序中的 lang/{lang}/passwords.php 语言文件决定。每个可能的状态值条目都位于该语言文件中。如果您的应用程序没有 lang 目录,可以使用 lang:publish Artisan 命令来创建它。

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

删除过期的令牌

密码重置令牌过期后仍然会保留在数据库中。但是,您可以使用 auth:clear-resets Artisan 命令轻松删除这些记录:

php artisan auth:clear-resets

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

use Illuminate\Support\Facades\Schedule;

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

自定义

重置链接自定义

您可以使用 ResetPassword 通知类提供的 createUrlUsing 方法自定义密码重置链接 URL。该方法接受一个闭包,闭包接收一个正在接收通知的用户实例以及密码重置链接的令牌。通常,您应该在应用程序的 App\Providers\AppServiceProvider 服务提供者的 boot 方法中调用此方法:

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

/**
 * 启动任何应用程序服务。
 */
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;

/**
 * 向用户发送密码重置通知。
 *
 * @param  string  $token
 */
public function sendPasswordResetNotification($token): void
{
    $url = 'https://example.com/reset-password?token='.$token;

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