laravel
6. 安全
重置密码

介绍

大多数 Web 应用程序都为用户提供了一种重置其忘记的密码的方法。Laravel 为发送密码重置链接和安全地重置密码提供了便捷的服务,而不是迫使您为创建的每个应用程序手动重新实现此功能。

[!NOTE]
想要快速上手吗?在全新的 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 标头的内容如何。此外,在 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']);  // 验证请求中的 'email' 属性,要求必填且为有效的电子邮件格式

    $status = Password::sendResetLink(
        $request->only('email')
    );  // 使用 Laravel 的内置“密码代理”(通过 `Password` 外观)向用户发送密码重置链接

    return $status === Password::RESET_LINK_SENT
               ? back()->with(['status' => __($status)])
                : back()->withErrors(['email' => __($status)]);  // 根据 `sendResetLink` 方法的返回状态进行相应的处理
})->middleware('guest')->name('password.email');  // 应用 'guest' 中间件,并为该路由命名为 'password.email'

在继续之前,让我们更详细地研究一下这个路由。首先,验证请求的 email 属性。接下来,我们将使用 Laravel 的内置“密码代理”(通过 Password 外观)向用户发送密码重置链接。密码代理将负责通过给定的字段(在本例中为电子邮件地址)检索用户,并通过 Laravel 的内置通知系统向用户发送密码重置链接。

sendResetLink 方法返回一个“状态”标识。为了向用户显示关于其请求状态的友好消息,可以使用 Laravel 的本地化助手来翻译此状态。密码重置状态的翻译由应用程序的 lang/{lang}/passwords.php 语言文件确定。在 passwords 语言文件中为状态标识的每个可能值都有一个条目。

[!NOTE]
默认情况下,Laravel 应用程序框架不包含 lang 目录。如果您想要自定义 Laravel 的语言文件,可以通过 lang:publish Artisan 命令进行发布。

您可能想知道,当调用 Password 外观的 sendResetLink 方法时,Laravel 如何从应用程序的数据库中检索用户记录。Laravel 密码代理使用您的认证系统的“用户提供程序”来检索数据库记录。密码代理使用的用户提供程序在 config/auth.php 配置文件的 passwords 配置数组中进行配置。要了解有关编写自定义用户提供程序的更多信息,请查阅认证文档

[!NOTE]
当手动实现密码重置时,您需要自己定义视图和路由的内容。如果您想要包含所有必要的认证和验证逻辑的脚手架,可以查看Laravel 应用程序启动工具包

重置密码

密码重置表单

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

Route::get('/reset-password/{token}', function (string $token) {
    return view('auth.reset-password', ['token' => $token]);  // 返回一个视图,视图名为 'auth.reset-password',并传递一个包含 'token' 值的数组
})->middleware('guest')->name('password.reset');  // 应用 'guest' 中间件,并为该路由命名为 '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' => '必填',
        'email' => '必填且为有效的电子邮件地址',
        'password' => '必填且至少 8 个字符,并需确认',
    ]);

    $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 外观)来验证密码重置请求的凭据。

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

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

在继续之前,您可能想知道当调用 Password 外观的 reset 方法时,Laravel 如何知道如何从您的应用程序的数据库中检索用户记录。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));
}