Laravel предоставляет полноценный механизм сброса пароля: от генерации токена и отправки email до сброса и обновления пароля. Система построена на концепции Password Broker.
Архитектура сброса пароля
Процесс состоит из четырёх шагов:
- Запрос -- пользователь вводит email
- Отправка -- Laravel генерирует токен и отправляет email со ссылкой
- Переход -- пользователь кликает ссылку и попадает на форму
- Сброс -- пользователь вводит новый пароль, токен проверяется и пароль обновляется
Конфигурация
// config/auth.php
'passwords' => [
'users' => [
'provider' => 'users', // User provider name
'table' => 'password_reset_tokens', // Table for tokens
'expire' => 60, // Token expires in 60 minutes
'throttle' => 60, // 60 seconds between requests
],
],
Миграция для токенов
<?php
declare(strict_types=1);
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('password_reset_tokens', function (Blueprint $table) {
$table->string('email')->primary();
$table->string('token');
$table->timestamp('created_at')->nullable();
});
}
};
Реализация маршрутов
1. Форма запроса сброса (Forgot Password)
<?php
// routes/web.php
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Facades\Route;
// Show forgot password form
Route::get('/forgot-password', function () {
return view('auth.forgot-password');
})->middleware('guest')->name('password.request');
// Handle forgot password form submission
Route::post('/forgot-password', function (Request $request) {
$request->validate([
'email' => ['required', 'email'],
]);
// Send the password reset link
$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');
2. Форма сброса пароля (Reset Password)
// Show reset password form
Route::get('/reset-password/{token}', function (string $token) {
return view('auth.reset-password', ['token' => $token]);
})->middleware('guest')->name('password.reset');
// Handle reset password form submission
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 (\App\Models\User $user, string $password) {
$user->forceFill([
'password' => \Illuminate\Support\Facades\Hash::make($password),
])->setRememberToken(\Illuminate\Support\Str::random(60));
$user->save();
event(new \Illuminate\Auth\Events\PasswordReset($user));
}
);
return $status === Password::PASSWORD_RESET
? redirect()->route('login')->with('status', __($status))
: back()->withErrors(['email' => [__($status)]]);
})->middleware('guest')->name('password.update');
Password Broker
Password Broker -- это центральный компонент, управляющий процессом сброса пароля.
Статусы Password Broker
use Illuminate\Support\Facades\Password;
// Possible statuses:
Password::RESET_LINK_SENT; // 'passwords.sent'
Password::PASSWORD_RESET; // 'passwords.reset'
Password::INVALID_USER; // 'passwords.user'
Password::INVALID_TOKEN; // 'passwords.token'
Password::RESET_THROTTLED; // 'passwords.throttled'
Работа с несколькими Brokers
// config/auth.php
'passwords' => [
'users' => [
'provider' => 'users',
'table' => 'password_reset_tokens',
'expire' => 60,
'throttle' => 60,
],
'admins' => [
'provider' => 'admins',
'table' => 'admin_password_reset_tokens',
'expire' => 30,
'throttle' => 120,
],
],
// Using a specific broker
$status = Password::broker('admins')->sendResetLink(
$request->only('email')
);
Реализация для API (SPA)
<?php
declare(strict_types=1);
namespace App\Http\Controllers\Auth;
use App\Http\Controllers\Controller;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Password;
use Illuminate\Support\Str;
use Illuminate\Validation\Rules\Password as PasswordRule;
final class PasswordResetController extends Controller
{
/**
* Send a password reset link.
*/
public function sendResetLink(Request $request): JsonResponse
{
$request->validate([
'email' => ['required', 'email'],
]);
$status = Password::sendResetLink(
$request->only('email')
);
if ($status === Password::RESET_LINK_SENT) {
return response()->json([
'message' => 'Ссылка для сброса пароля отправлена на ваш email.',
]);
}
return response()->json([
'message' => __($status),
], 422);
}
/**
* Reset the user's password.
*/
public function resetPassword(Request $request): JsonResponse
{
$request->validate([
'token' => ['required'],
'email' => ['required', 'email'],
'password' => [
'required',
'confirmed',
PasswordRule::min(8)
->letters()
->mixedCase()
->numbers()
->symbols(),
],
]);
$status = Password::reset(
$request->only('email', 'password', 'password_confirmation', 'token'),
function ($user, string $password) {
$user->forceFill([
'password' => Hash::make($password),
'remember_token' => Str::random(60),
])->save();
event(new \Illuminate\Auth\Events\PasswordReset($user));
}
);
if ($status === Password::PASSWORD_RESET) {
return response()->json([
'message' => 'Пароль успешно обновлён.',
]);
}
return response()->json([
'message' => __($status),
], 422);
}
}
Кастомизация уведомления
Через callback
<?php
declare(strict_types=1);
namespace App\Providers;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Support\ServiceProvider;
final class AppServiceProvider extends ServiceProvider
{
public function boot(): void
{
// Customize the reset password email
ResetPassword::toMailUsing(function (object $notifiable, string $token) {
$url = url(route('password.reset', [
'token' => $token,
'email' => $notifiable->getEmailForPasswordReset(),
], false));
return (new MailMessage())
->subject('Сброс пароля')
->greeting("Здравствуйте, {$notifiable->name}!")
->line('Вы получили это письмо, потому что запросили сброс пароля.')
->action('Сбросить пароль', $url)
->line("Ссылка действительна в течение {config('auth.passwords.users.expire')} минут.")
->line('Если вы не запрашивали сброс пароля, проигнорируйте это письмо.')
->salutation('С уважением, Команда');
});
}
}
Через URL callback (для SPA)
use Illuminate\Auth\Notifications\ResetPassword;
ResetPassword::createUrlUsing(function (object $notifiable, string $token) {
return config('app.frontend_url') . '/reset-password?' . http_build_query([
'token' => $token,
'email' => $notifiable->getEmailForPasswordReset(),
]);
});
Полностью кастомное уведомление
<?php
declare(strict_types=1);
namespace App\Notifications;
use Illuminate\Auth\Notifications\ResetPassword as BaseResetPassword;
use Illuminate\Notifications\Messages\MailMessage;
final class CustomResetPassword extends BaseResetPassword
{
protected function buildMailMessage(string $url): MailMessage
{
return (new MailMessage())
->subject('Восстановление доступа')
->markdown('emails.password-reset', [
'url' => $url,
'expire' => config('auth.passwords.users.expire'),
]);
}
}
// Override in User model
final class User extends Authenticatable
{
public function sendPasswordResetNotification(mixed $token): void
{
$this->notify(new \App\Notifications\CustomResetPassword($token));
}
}
Безопасность токенов
Как работает токен
// 1. When sending reset link:
// - A random 64-character token is generated
// - The HASH of the token is stored in password_reset_tokens table
// - The PLAIN token is sent to the user via email
// 2. When resetting password:
// - Laravel retrieves the hash from the table
// - Uses Hash::check() to compare the plain token with the stored hash
// - If valid and not expired, the password is reset
// 3. After reset:
// - The token record is deleted from the table
Throttling
// The 'throttle' config value (in seconds) prevents
// sending reset links too frequently.
// Default: 60 seconds between requests.
// If a user requests a link within the throttle period:
$status = Password::sendResetLink($request->only('email'));
// Returns: Password::RESET_THROTTLED
Очистка устаревших токенов
# Remove expired tokens
php artisan auth:clear-resets
# Schedule periodic cleanup
# In routes/console.php:
Schedule::command('auth:clear-resets')->everyFifteenMinutes();
Password Confirmation
Laravel также предоставляет функцию подтверждения пароля для защиты чувствительных действий.
// routes/web.php
Route::get('/settings', function () {
return view('settings');
})->middleware(['auth', 'password.confirm']);
// The user must re-enter their password before accessing this route
// After confirming, they have a grace period (default: 3 hours)
// config/auth.php
'password_timeout' => env('AUTH_PASSWORD_TIMEOUT', 10800), // 3 hours in seconds
Для API
Route::middleware(['auth:sanctum'])->group(function () {
Route::post('/confirm-password', function (Request $request) {
if (! Hash::check($request->password, $request->user()->password)) {
return response()->json([
'message' => 'Неверный пароль.',
], 422);
}
$request->session()->passwordConfirmedAt(now());
return response()->json(['message' => 'Пароль подтверждён.']);
});
// Protected by password confirmation
Route::delete('/account', function (Request $request) {
// Delete account...
})->middleware('password.confirm');
});
Тестирование
<?php
declare(strict_types=1);
namespace Tests\Feature;
use App\Models\User;
use Illuminate\Auth\Notifications\ResetPassword;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Facades\Notification;
use Illuminate\Support\Facades\Password;
use Tests\TestCase;
final class PasswordResetTest extends TestCase
{
public function test_reset_link_can_be_requested(): void
{
Notification::fake();
$user = User::factory()->create();
$response = $this->postJson('/forgot-password', [
'email' => $user->email,
]);
Notification::assertSentTo($user, ResetPassword::class);
}
public function test_password_can_be_reset(): void
{
Notification::fake();
$user = User::factory()->create();
// Request reset link
$this->postJson('/forgot-password', ['email' => $user->email]);
Notification::assertSentTo($user, ResetPassword::class, function ($notification) use ($user) {
// Use the token from the notification
$response = $this->postJson('/reset-password', [
'token' => $notification->token,
'email' => $user->email,
'password' => 'new-password-123',
'password_confirmation' => 'new-password-123',
]);
$response->assertOk();
// Verify the password was updated
$this->assertTrue(
Hash::check('new-password-123', $user->fresh()->password)
);
return true;
});
}
public function test_invalid_token_is_rejected(): void
{
$user = User::factory()->create();
$response = $this->postJson('/reset-password', [
'token' => 'invalid-token',
'email' => $user->email,
'password' => 'new-password-123',
'password_confirmation' => 'new-password-123',
]);
$response->assertStatus(422);
}
public function test_reset_link_is_throttled(): void
{
$user = User::factory()->create();
// First request
$this->postJson('/forgot-password', ['email' => $user->email]);
// Second request within throttle window
$response = $this->postJson('/forgot-password', [
'email' => $user->email,
]);
$response->assertStatus(422);
}
public function test_expired_token_is_rejected(): void
{
$user = User::factory()->create();
// Create a token
$token = Password::createToken($user);
// Travel forward in time past expiration
$this->travel(61)->minutes();
$response = $this->postJson('/reset-password', [
'token' => $token,
'email' => $user->email,
'password' => 'new-password-123',
'password_confirmation' => 'new-password-123',
]);
$response->assertStatus(422);
}
}