Skip to content
全部文档

重置密码

简介

大多数 Web 应用都会提供用户重置遗忘密码的方式。Laravel 提供了便捷的服务,用于发送密码重置链接并安全地重置密码,而无需你在每个应用中亲手重写该功能。

INFO

想快速上手?在全新的 Laravel 应用中安装一个 Laravel 应用起步套件。起步套件会搭建完整的认证系统,包括遗忘密码重置。

配置

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

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

  • `database` - 密码重置数据存储在关系型数据库中。
  • `cache` - 密码重置数据存储在你的某个基于缓存的存储中。

驱动前置条件

数据库

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

缓存

也有可用的缓存驱动来处理密码重置,无需专用数据库表。条目以用户邮箱地址为键,因此请确保应用其他地方没有将邮箱地址用作缓存键:

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

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

模型准备

在使用 Laravel 的密码重置功能之前,应用的 App\Models\User 模型必须使用 Illuminate\Notifications\Notifiable trait。通常,新建 Laravel 应用时默认创建的 App\Models\User 模型已包含该 trait。

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

配置受信任主机

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

通常,你应配置 Nginx 或 Apache 等 Web 服务器,仅将匹配给定主机名的请求发送到应用。但若无法直接定制 Web 服务器,而又需要让 Laravel 仅响应特定主机名,可在应用的 bootstrap/app.php 文件中使用 trustHosts 中间件方法。当应用提供密码重置功能时,这一点尤为重要。

要进一步了解该中间件方法,请参阅 TrustHosts 中间件文档

路由

要正确实现允许用户重置密码的支持,需要定义若干路由。首先需要一对路由,处理用户通过邮箱请求密码重置链接;其次还需要一对路由,处理用户访问邮件中的密码重置链接并完成重置表单后真正重置密码。

首先定义请求密码重置链接所需的路由。先定义一条返回密码重置链接请求表单视图的路由:

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

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

接下来定义一条路由,处理「忘记密码」视图的表单提交请求。该路由负责校验邮箱地址,并向对应用户发送密码重置请求:

php
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 属性。接着通过 Password facade 使用 Laravel 内置的「密码代理」向用户发送密码重置链接。密码代理会按给定字段(此处为邮箱地址)检索用户,并通过 Laravel 内置的通知系统向用户发送密码重置链接。

sendResetLink 方法返回一个「状态」slug。可使用 Laravel 的本地化助手翻译该状态,以便向用户展示关于请求状态的友好消息。密码重置状态的翻译由应用的 lang/{lang}/passwords.php 语言文件决定。passwords 语言文件中包含每个可能状态 slug 值的条目。

INFO

默认情况下,Laravel 应用骨架不包含 lang 目录。若要自定义 Laravel 语言文件,可通过 lang:publish Artisan 命令发布它们。

你可能想知道:调用 Password facade 的 sendResetLink 方法时,Laravel 如何从应用数据库中检索用户记录。Laravel 密码代理会利用认证系统的「用户提供者」检索数据库记录。密码代理使用的用户提供者在 config/auth.phppasswords 配置数组中配置。要了解如何编写自定义用户提供者,请参阅认证文档

INFO

手动实现密码重置时,你需要自行定义视图与路由的内容。若希望获得包含全部必要认证与验证逻辑的脚手架,请参阅 Laravel 应用起步套件

重置密码

密码重置表单

接下来定义用户点击邮件中的密码重置链接并提供新密码后,真正重置密码所需的路由。先定义在用户点击重置密码链接时显示重置表单的路由。该路由会接收一个 token 参数,稍后用于校验密码重置请求:

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

该路由返回的视图应显示包含 emailpasswordpassword_confirmation 字段以及隐藏 token 字段的表单;隐藏字段应包含路由收到的秘密 $token 值。

处理表单提交

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

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

继续之前,我们再详细看看这条路由。首先校验请求的 tokenemailpassword 属性。接着通过 Password facade 使用 Laravel 内置的「密码代理」校验密码重置请求凭证。

若提供给密码代理的令牌、邮箱地址与密码有效,则会调用传给 reset 方法的闭包。该闭包会收到用户实例以及密码重置表单提供的明文密码,你可以在其中更新数据库中的用户密码。

reset 方法返回一个「状态」slug。可使用 Laravel 的本地化助手翻译该状态,以便向用户展示关于请求状态的友好消息。密码重置状态的翻译由应用的 lang/{lang}/passwords.php 语言文件决定。passwords 语言文件中包含每个可能状态 slug 值的条目。若应用没有 lang 目录,可使用 lang:publish Artisan 命令创建。

继续之前,你可能想知道:调用 Password facade 的 reset 方法时,Laravel 如何从应用数据库中检索用户记录。Laravel 密码代理会利用认证系统的「用户提供者」检索数据库记录。密码代理使用的用户提供者在 config/auth.phppasswords 配置数组中配置。要了解如何编写自定义用户提供者,请参阅认证文档

删除过期令牌

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

shell
php artisan auth:clear-resets

若希望自动化该过程,可考虑将命令加入应用的调度器

php
use Illuminate\Support\Facades\Schedule;

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

自定义

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

php
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,并向用户发送通知:

php
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));
}