> ## 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에서 비밀번호 재설정 기능을 구현하는 방법

## 시작하기

비밀번호 재설정은 웹 애플리케이션에 빠질 수 없는 인증 플로우입니다. 스타터 킷을 사용하면 이 기능은 자동으로 구축되지만, 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>토큰이 포함된 URL"]
    D --> E["재설정 폼 표시<br>GET /reset-password/{token}"]
    E --> F["비밀번호 변경<br>POST /reset-password"]
    F --> G["로그인 화면으로<br>리다이렉트"]
```

## 설정

비밀번호 재설정 설정은 `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` — 토큰의 유효 기간(분). 기본값은 60분
* `throttle` — 재전송을 제한할 때까지의 대기 시간(초)

## 드라이버

### database 드라이버

기본 드라이버입니다. 비밀번호 재설정 토큰을 데이터베이스의 `password_reset_tokens` 테이블에 저장합니다. 이 테이블은 Laravel의 기본 마이그레이션(`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 드라이버

<Tip>
  Laravel 11 이상에서 사용 가능한 새로운 옵션입니다. 데이터베이스 테이블이 필요 없기 때문에 간단한 구성으로 비밀번호 재설정을 구현할 수 있습니다.
</Tip>

`cache` 드라이버는 캐시 스토어에 토큰을 저장합니다. `password_reset_tokens` 테이블 마이그레이션이 필요 없게 됩니다. 토큰은 사용자의 이메일 주소를 키로 저장되므로, 앱 내 다른 위치에서 이메일 주소를 캐시 키로 사용하지 않도록 주의하세요.

```php theme={null}
'passwords' => [
    'users' => [
        'driver' => 'cache',
        'provider' => 'users',
        'store' => 'passwords', // 옵션: 전용 캐시 스토어
        'expire' => 60,
        'throttle' => 60,
    ],
],
```

`store` 키에 전용 캐시 스토어를 지정하면 `php artisan cache:clear`로 재설정 데이터가 지워지는 것을 방지할 수 있습니다. 지정하는 값은 `config/cache.php`에 설정된 스토어 이름에 대응시킵니다.

## 모델 준비

비밀번호 재설정 기능을 사용하려면 `App\Models\User` 모델에 두 개의 트레이트가 필요합니다.

```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` — 비밀번호 재설정 토큰의 생성·검증에 필요한 메서드를 제공

<Info>
  Laravel의 기본 `User` 모델에는 이미 이 트레이트들이 포함되어 있습니다. 신규 설치의 경우 추가할 필요가 없습니다.
</Info>

## 라우팅 구현

비밀번호 재설정에는 네 개의 라우트가 필요합니다.

### 1. 재설정 링크 요청 폼

이메일 주소를 입력하는 폼을 표시합니다.

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

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

해당하는 Blade 뷰:

```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`    | 지정한 이메일의 사용자를 찾을 수 없음 |
| `Password::RESET_THROTTLED` | 전송이 제한됨(스로틀링)         |

### 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 뷰:

```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` | 토큰이 무효이거나 만료됨         |
| `Password::INVALID_USER`  | 지정한 이메일의 사용자를 찾을 수 없음 |

## 토큰의 유효 기간

`config/auth.php`의 `expire` 옵션으로 토큰의 유효 기간을 분 단위로 설정할 수 있습니다. 기본값은 60분입니다.

```php theme={null}
'passwords' => [
    'users' => [
        'driver' => 'database',
        'expire' => 60, // 60분 후 만료
        'throttle' => 60,
    ],
],
```

`database` 드라이버를 사용하는 경우, 만료된 토큰은 데이터베이스에 계속 남습니다. 정기적으로 정리하려면 다음 Artisan 명령을 사용합니다.

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

스케줄러로 자동화하는 것을 권장합니다.

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

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

## 커스터마이징

### 커스텀 알림 사용

비밀번호 재설정 메일을 커스터마이즈하려면 `User` 모델에서 `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` 헤더를 사용해 생성됩니다. 부정한 호스트에서 오는 요청을 방지하기 위해 `bootstrap/app.php`에서 Trusted Hosts 설정을 권장합니다.

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

<Warning>
  비밀번호 재설정 기능을 구현할 경우 Trusted Hosts 설정을 반드시 확인하세요. 설정이 미흡하면 호스트 헤더 인젝션 공격의 위험이 있습니다.
</Warning>

## 정리

| 하고 싶은 일        | 방법                                             |
| -------------- | ---------------------------------------------- |
| 재설정 링크 전송      | `Password::sendResetLink(['email' => $email])` |
| 비밀번호 재설정       | `Password::reset($credentials, $callback)`     |
| 테이블 없이 재설정     | `cache` 드라이버 설정                                |
| 만료 토큰 삭제       | `php artisan auth:clear-resets`                |
| 메일 알림 커스터마이즈   | `sendPasswordResetNotification` 오버라이드          |
| 재설정 URL 커스터마이즈 | `ResetPassword::createUrlUsing()`              |

## 다음 단계

<Columns cols={2}>
  <Card title="인증 입문" icon="lock" href="/ko/authentication">
    Laravel의 인증 시스템 전체 구조를 학습합니다.
  </Card>

  <Card title="알림" icon="bell" href="/ko/notifications">
    메일 알림의 커스터마이징 방법을 자세히 학습합니다.
  </Card>
</Columns>


## Related topics

- [해시(Hashing)](/ko/hashing.md)
- [Laravel Prompts](/ko/prompts.md)
- [Laravel Fortify와 스타터 킷](/ko/advanced/fortify.md)
- [스타터 킷](/ko/starter-kits.md)
- [설정](/ko/configuration.md)
