> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# 密碼重設

> 在 Laravel 中實作密碼重設功能的方法

## 前言

密碼重設是 Web 應用程式不可或缺的認證流程。使用起始套件（Starter Kit）時會自動建置此功能，但在僅提供 API 的專案或自製 UI 的專案中，需要手動實作。

**需要手動實作的情境：**

* 專為 API 提供的後端（前端為 SPA、行動應用程式）
* 不使用起始套件而自行建置認證 UI 時
* 想完全自訂郵件通知或重設 URL 的設計時

<Info>
  若使用起始套件（`laravel new`）建立專案，密碼重設功能已經實作完成。本頁面說明在沒有起始套件的情況下的實作方法。
</Info>

密碼重設的整體流程如下：

```mermaid theme={null}
flowchart TD
    A["使用者"] --> B["輸入電子郵件<br>GET /forgot-password"]
    B --> C["寄送重設連結<br>POST /forgot-password"]
    C --> D["收到郵件<br>含 token 的 URL"]
    D --> E["顯示重設表單<br>GET /reset-password/{token}"]
    E --> F["變更密碼<br>POST /reset-password"]
    F --> G["導向登入頁面"]
```

## 設定

密碼重設的設定由 `config/auth.php` 的 `passwords` 鍵管理。

```php theme={null}
// config/auth.php

'passwords' => [
    'users' => [
        'driver' => 'database',
        'provider' => 'users',
        'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
        'expire' => 60,
        'throttle' => 60,
    ],
],
```

* `driver` — 資料儲存方式（`database` 或 `cache`）
* `expire` — token 的有效期限（分鐘）。預設為 60 分鐘
* `throttle` — 限制重送前的等待時間（秒）

## Driver

### database driver

預設 driver。將密碼重設 token 存到資料庫的 `password_reset_tokens` 資料表。此資料表包含在 Laravel 的預設 migration（`0001_01_01_000000_create_users_table.php`）中。

```php theme={null}
'passwords' => [
    'users' => [
        'driver' => 'database',
        'provider' => 'users',
        'table' => env('AUTH_PASSWORD_RESET_TOKEN_TABLE', 'password_reset_tokens'),
        'expire' => 60,
        'throttle' => 60,
    ],
],
```

### cache driver

<Tip>
  這是 Laravel 11 起可用的新選項。由於不需要資料表，可以用更簡潔的設定實作密碼重設。
</Tip>

`cache` driver 將 token 存到快取 store。因此不需要 `password_reset_tokens` 資料表的 migration。因 token 以使用者的電子郵件為 key 儲存，請注意不要在應用程式的其他地方以同樣的 email 作為快取 key。

```php theme={null}
'passwords' => [
    'users' => [
        'driver' => 'cache',
        'provider' => 'users',
        'store' => 'passwords', // 選項：專用的快取 store
        'expire' => 60,
        'throttle' => 60,
    ],
],
```

在 `store` 指定專用快取 store 可以避免 `php artisan cache:clear` 時重設資料被清除。指定的值需對應到 `config/cache.php` 內設定的 store 名稱。

## Model 的準備

要使用密碼重設功能，`App\Models\User` model 需要兩個 trait。

```php theme={null}
<?php

namespace App\Models;

use Illuminate\Auth\Passwords\CanResetPassword;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;

class User extends Authenticatable
{
    use Notifiable, CanResetPassword;

    // ...
}
```

* `Notifiable` — 寄送郵件通知時所需
* `CanResetPassword` — 提供產生／驗證密碼重設 token 所需的方法

<Info>
  Laravel 預設的 `User` model 已經包含這些 trait。全新安裝時無需再新增。
</Info>

## 實作路由

密碼重設需要 4 條路由。

### 1. 重設連結的請求表單

顯示輸入電子郵件的表單。

```php theme={null}
// routes/web.php

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

對應的 Blade view：

```blade theme={null}
{{-- resources/views/auth/forgot-password.blade.php --}}

<form method="POST" action="/forgot-password">
    @csrf

    <div>
        <label for="email">電子郵件</label>
        <input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus>
        @error('email')
            <span>{{ $message }}</span>
        @enderror
    </div>

    @if (session('status'))
        <div>{{ session('status') }}</div>
    @endif

    <button type="submit">寄送重設連結</button>
</form>
```

### 2. 寄送重設連結的處理

接收表單送出，使用 `Password::sendResetLink()` 寄送重設郵件。

```php theme={null}
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');
```

`Password::sendResetLink()` 會回傳狀態字串。

| 狀態常數                        | 說明                 |
| --------------------------- | ------------------ |
| `Password::ResetLinkSent`   | 已寄送重設連結            |
| `Password::INVALID_USER`    | 找不到指定 email 的使用者   |
| `Password::RESET_THROTTLED` | 寄送遭到限制（throttling） |

### 3. 密碼重設表單

顯示表單讓點擊郵件連結的使用者輸入新密碼。

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

對應的 Blade view：

```blade theme={null}
{{-- resources/views/auth/reset-password.blade.php --}}

<form method="POST" action="/reset-password">
    @csrf

    <input type="hidden" name="token" value="{{ $token }}">

    <div>
        <label for="email">電子郵件</label>
        <input id="email" type="email" name="email" value="{{ old('email') }}" required autofocus>
        @error('email')
            <span>{{ $message }}</span>
        @enderror
    </div>

    <div>
        <label for="password">新密碼</label>
        <input id="password" type="password" name="password" required>
        @error('password')
            <span>{{ $message }}</span>
        @enderror
    </div>

    <div>
        <label for="password_confirmation">確認密碼</label>
        <input id="password_confirmation" type="password" name="password_confirmation" required>
    </div>

    <button type="submit">重設密碼</button>
</form>
```

### 4. 密碼重設執行處理

接收表單，並以 `Password::reset()` 更新密碼。

```php theme={null}
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');
```

`Password::reset()` 的狀態常數：

| 狀態常數                      | 說明               |
| ------------------------- | ---------------- |
| `Password::PasswordReset` | 密碼重設成功           |
| `Password::INVALID_TOKEN` | token 無效或已過期     |
| `Password::INVALID_USER`  | 找不到指定 email 的使用者 |

## Token 的有效期限

可透過 `config/auth.php` 的 `expire` 選項以分鐘為單位設定 token 的有效期限。預設為 60 分鐘。

```php theme={null}
'passwords' => [
    'users' => [
        'driver' => 'database',
        'expire' => 60, // 60 分鐘後過期
        'throttle' => 60,
    ],
],
```

若使用 `database` driver，過期 token 會殘留在資料庫中。可用下列 Artisan 指令定期清除：

```shell theme={null}
php artisan auth:clear-resets
```

建議透過排程器自動化：

```php theme={null}
use Illuminate\Support\Facades\Schedule;

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

## 自訂

### 使用自訂通知

要自訂密碼重設郵件時，在 `User` model 覆寫 `sendPasswordResetNotification` 方法。

```php theme={null}
use App\Notifications\ResetPasswordNotification;

class User extends Authenticatable
{
    use Notifiable, CanResetPassword;

    /**
     * 寄送密碼重設通知
     */
    public function sendPasswordResetNotification($token): void
    {
        $url = 'https://example.com/reset-password?token='.$token;

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

### 自訂重設連結 URL

在 `AppServiceProvider` 的 `boot` 方法中使用 `ResetPassword::createUrlUsing()`，可變更重設連結的 URL。適用於欲導向 SPA 等其他來源的前端時。

```php theme={null}
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;
    });
}
```

### Trusted Hosts 設定

密碼重設連結是根據 HTTP 請求的 `Host` header 產生。為避免來自非法 host 的請求，建議在 `bootstrap/app.php` 設定 Trusted Hosts。

```php theme={null}
->withMiddleware(function (Middleware $middleware) {
    $middleware->trustHosts(at: ['example.com']);
})
```

<Warning>
  在實作密碼重設功能時，請務必確認 Trusted Hosts 的設定。設定不完備會有 host header injection 攻擊的風險。
</Warning>

## 總結

| 需求          | 方法                                             |
| ----------- | ---------------------------------------------- |
| 寄送重設連結      | `Password::sendResetLink(['email' => $email])` |
| 重設密碼        | `Password::reset($credentials, $callback)`     |
| 不使用資料表進行重設  | 設定 `cache` driver                              |
| 刪除過期的 token | `php artisan auth:clear-resets`                |
| 自訂郵件通知      | 覆寫 `sendPasswordResetNotification`             |
| 自訂重設 URL    | `ResetPassword::createUrlUsing()`              |

## 後續步驟

<Columns cols={2}>
  <Card title="認證入門" icon="lock" href="/zh-TW/authentication">
    了解 Laravel 認證系統整體的運作機制。
  </Card>

  <Card title="通知" icon="bell" href="/zh-TW/notifications">
    詳細了解郵件通知的自訂方式。
  </Card>
</Columns>


## Related topics

- [字串操作（Str 類別）](/zh-TW/strings.md)
- [Laravel Fortify 與啟動套件](/zh-TW/advanced/fortify.md)
- [從 laravel/ui 遷移到 Fortify 的指南](/zh-TW/blog/ui-to-fortify.md)
- [起始套件（Starter Kit）](/zh-TW/starter-kits.md)
- [認證入門](/zh-TW/authentication.md)
