密码重置
简介
大多数 Web 应用都会为用户提供一种方式来重置忘记的密码。与其让你在每个应用里都手动实现一遍,Laravel 直接提供了发送密码重置链接和安全重置密码的便捷服务。
信息
想快速上手?在一个全新的 Laravel 应用中安装 Laravel 应用起步套件。Laravel 起步套件会帮你搭好整套认证系统,包括忘记密码的重置流程。
配置
应用的密码重置配置文件位于 config/auth.php。别忘了看看这个文件里可用的选项。Laravel 默认配置为使用 database 密码重置驱动。
密码重置的 driver 配置项决定了密码重置数据存到哪里。Laravel 内置了两个驱动:
database——把密码重置数据存到关系型数据库。cache——把密码重置数据存到某个基于缓存的存储中。
驱动前置条件
数据库 {#database}
使用默认的 database 驱动时,必须建一张表来存储应用的密码重置 token。这张表通常已经包含在 Laravel 默认的 0001_01_01_000000_create_users_table.php 数据库迁移中。
缓存 {#cache}
处理密码重置也可以用缓存驱动,这样就不需要专门的数据库表。数据条目以用户邮箱作为键,所以要确保你没在应用其他地方也用邮箱作为缓存键:
'passwords' => [
'users' => [
'driver' => 'cache',
'provider' => 'users',
'store' => 'passwords', // Optional...
'expire' => 60,
'throttle' => 60,
],
],
为了防止 artisan cache:clear 把你的密码重置数据也一并清空,你可以用 store 配置项指定一个独立的缓存存储。这个值应对应 config/cache.php 配置文件中已配置的某个 store。
模型准备
在使用 Laravel 的密码重置功能之前,应用的 App\Models\User 模型必须使用 Illuminate\Notifications\Notifiable trait。通常,这个 trait 在新 Laravel 应用创建时就已经包含在默认的 App\Models\User 模型里了。
接下来,请确认你的 App\Models\User 模型实现了 Illuminate\Contracts\Auth\CanResetPassword 契约。框架自带的 App\Models\User 模型已经实现了这个接口,并使用 Illuminate\Auth\Passwords\CanResetPassword trait 提供实现该接口所需的方法。
配置可信主机
默认情况下,无论 HTTP 请求的 Host 头是什么,Laravel 都会响应它收到的所有请求。此外,Host 头的值还会在 Web 请求期间用于生成应用的绝对 URL。
一般来说,你应该配置 Web 服务器(比如 Nginx 或 Apache),只把匹配指定主机名的请求发到应用。但如果你没办法直接改 Web 服务器配置,需要让 Laravel 只对某些主机名响应,可以在应用的 bootstrap/app.php 文件中使用 trustHosts() 中间件方法。这一点在应用提供密码重置功能时尤其重要。
关于这个中间件方法的更多内容,请查阅 TrustHosts 中间件文档。
路由
要完整实现让用户重置密码的功能,我们需要定义若干条路由。首先,需要一对路由处理用户通过邮箱地址请求密码重置链接的流程。其次,需要另一对路由处理用户点开邮件中的密码重置链接、填完密码重置表单之后的实际密码重置流程。
请求密码重置链接
密码重置链接请求表单 {#the-password-reset-link-request-form}
首先,我们定义请求密码重置链接所需的路由。先从返回一个包含密码重置链接请求表单的视图的路由开始:
Route::get('/forgot-password', function () {
return view('auth.forgot-password');
})->middleware('guest')->name('password.request');
这条路由返回的视图应该包含一个 email 字段的表单,让用户为指定邮箱请求密码重置链接。
处理表单提交 {#password-reset-link-handling-the-form-submission}
接下来,我们定义一条路由,处理来自「忘记密码」视图的表单提交请求。这条路由负责校验邮箱地址,并把密码重置请求发送给对应的用户:
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 门面使用 Laravel 内置的「password broker」(密码代理)给用户发送密码重置链接。密码代理会根据给定字段(这里是邮箱地址)取出用户,并通过 Laravel 内置的通知系统给用户发送密码重置链接。
sendResetLink() 方法返回一个「状态」slug。你可以用 Laravel 的本地化辅助函数翻译这个状态,向用户展示友好的提示信息。密码重置状态的翻译由应用的 lang/{lang}/passwords.php 语言文件决定。状态 slug 的每一种可能值在 passwords 语言文件中都有对应条目。
信息
Laravel 应用骨架默认不包含 lang 目录。如果你想自定义 Laravel 的语言文件,可以用 lang:publish Artisan 命令把它们发布出来。
你可能会好奇,调用 Password 门面的 sendResetLink() 方法时,Laravel 是怎么从数据库里找到用户记录的。Laravel 密码代理利用认证系统的「用户提供者(user providers)」来获取数据库记录。密码代理使用的用户提供者在 config/auth.php 配置文件的 passwords 配置数组中配置。想了解如何编写自定义用户提供者,请查阅认证文档。
信息
手动实现密码重置时,你需要自己定义视图和路由的内容。如果你想要一个开箱即用的脚手架,包含所有认证和验证逻辑,可以看看 Laravel 应用起步套件。
重置密码
密码重置表单 {#the-password-reset-form}
接下来,我们定义在用户点击邮件中的密码重置链接并填入新密码后,真正用于重置密码所需的路由。首先,定义展示重置密码表单的路由——用户点击重置密码链接时会看到这个表单。这条路由会接收一个 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 字段的值应该是我们路由收到的那个秘密 $token。
处理表单提交 {#password-reset-handling-the-form-submission}
当然,我们还需要定义一条路由来实际处理密码重置表单提交。这条路由负责校验传入的请求,并更新数据库中用户的密码:
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 属性。接着,通过 Password 门面使用 Laravel 内置的「password broker」来校验密码重置请求的凭据。
如果传给密码代理的 token、邮箱地址和密码都有效,传给 reset() 方法的闭包会被调用。闭包会收到用户实例和密码重置表单提供的明文密码,我们在闭包里就可以更新数据库中用户的密码了。
reset() 方法返回一个「状态」slug。你可以用 Laravel 的本地化辅助函数翻译这个状态,向用户展示友好的提示信息。密码重置状态的翻译由应用的 lang/{lang}/passwords.php 语言文件决定。状态 slug 的每一种可能值在 passwords 语言文件中都有对应条目。如果你的应用没有 lang 目录,可以用 lang:publish Artisan 命令来创建它。
同样地,你可能会好奇,调用 Password 门面的 reset() 方法时,Laravel 是怎么从数据库里找到用户记录的。Laravel 密码代理利用认证系统的「用户提供者」来获取数据库记录。密码代理使用的用户提供者在 config/auth.php 配置文件的 passwords 配置数组中配置。想了解如何编写自定义用户提供者,请查阅认证文档。
删除过期 Token
如果你用的是 database 驱动,已经过期的密码重置 token 仍会留在数据库里。不过你可以用 auth:clear-resets Artisan 命令轻松删除这些记录:
php artisan auth:clear-resets
如果想让这个过程自动化,可以考虑把这条命令加到应用的任务调度中:
use Illuminate\Support\Facades\Schedule;
Schedule::command('auth:clear-resets')->everyFifteenMinutes();
自定义
自定义重置链接
你可以通过 ResetPassword 通知类提供的 createUrlUsing() 方法自定义密码重置链接的 URL。这个方法接收一个闭包,闭包会收到接收通知的用户实例以及密码重置链接的 token。通常你应该在应用的 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));
}