前回はphpのハッシュ関数を紹介しました。今回は、Laravelではそれらの関数を使用してユーザーの認証や管理を行うのか見ていきます。
Laravel Breeze
Laravelのスターターキットのパッケージをcomposerで追加します。
$ composer require laravel/breeze --dev
そして、breezeをインストールします。選択がいくつかありますが、一番ベーシックなBlade with Alpineを指定します。
$ php artisan breeze:install
┌ Which Breeze stack would you like to install? ───────────────┐
│ Blade with Alpine │
└──────────────────────────────────────────────────────────────┘
このインストールで、以下のユーザーの認証に関わる機能が追加されます。
- ユーザーの登録
- ログイン
- パスワードの変更
それぞれに関してパスワードのハッシュ化を見ていきます。
ユーザーの登録
ユーザー登録の画面は、/registerにPOSTでアクセスし以下のコントローラのstore()の実行となります。
namespace App\Http\Controllers\Auth;
...
class RegisteredUserController extends Controller
{
...
public function store(Request $request): RedirectResponse
{
$request->validate([
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'lowercase', 'email', 'max:255', 'unique:'.User::class],
'password' => ['required', 'confirmed', Rules\Password::defaults()],
]);
$user = User::create([
'name' => $request->name,
'email' => $request->email,
'password' => Hash::make($request->password),
]);
event(new Registered($user));
Auth::login($user);
return redirect(route('dashboard', absolute: false));
}
}
DBのレコード作成時に、入力されたパスワードの値をハッシュ化しています。
'password' => Hash::make($request->password),
しかし、HashはLaravelのファサードであり、どのハッシュ方法を採用しているかわかりません。設定を見てみましょう。この設定ファイルがない場合は、php artisan config:publish hashingで作成が可能です。
return [
'driver' => env('HASH_DRIVER', 'bcrypt'),
'bcrypt' => [
'rounds' => env('BCRYPT_ROUNDS', 12),
'verify' => env('HASH_VERIFY', true),
'limit' => env('BCRYPT_LIMIT', null),
],
...
'rehash_on_login' => true,
];
上のように環境変数で指定がない限り、bcryptがドライバーとしてデフォルトとなり、最終的にはLaravelのパッケージの奥深くに定義されているBcryptHasherのクラスとなります。tinkerでは以下の実行でその確認ができます。
> Hash::getFacadeRoot()->driver();
= Illuminate\Hashing\BcryptHasher {#7560}
ということは、Hash::make()の実行は、以下のBcryptHasherのmake関数のコールとなります。前回で説明したように、php関数のpassword_hash()が使用されているのがわかります。
namespace Illuminate\Hashing;
...
class BcryptHasher extends AbstractHasher implements HasherContract
{
public function make(#[\SensitiveParameter] $value, array $options = [])
{
try {
if ($this->limit && strlen($value) > $this->limit) {
throw new InvalidArgumentException('Value is too long to hash. Value must be less than '.$this->limit.' bytes.');
}
$hash = password_hash($value, PASSWORD_BCRYPT, [
'cost' => $this->cost($options),
]);
} catch (Error) {
throw new RuntimeException('Bcrypt hashing not supported.');
}
return $hash;
}
...
}
ログイン
ログイン(/login)では、ユーザーがログイン画面で入力したログインとパスワードをもとに以下のコントローラのstore()が実行されます。
namespace App\Http\Controllers\Auth;
...
class AuthenticatedSessionController extends Controller
{
...
public function store(LoginRequest $request): RedirectResponse
{
$request->authenticate();
$request->session()->regenerate();
return redirect()->intended(route('dashboard', absolute: false));
}
...
}
ログインに対してのパスワードのチェックは、フォームリクエストであるLoginRequest::authenticate()で行われます。
namespace App\Http\Requests\Auth;
...
class LoginRequest extends FormRequest
{
...
public function authenticate(): void
{
$this->ensureIsNotRateLimited();
if (! Auth::attempt($this->only('email', 'password'), $this->boolean('remember'))) {
RateLimiter::hit($this->throttleKey());
throw ValidationException::withMessages([
'email' => trans('auth.failed'),
]);
}
RateLimiter::clear($this->throttleKey());
}
}
上のコードのAuth::attempt()は、最終的には先に登場したBcryptHash::check()のコールとなり、これまた前回で紹介したpassword_verify()のコールとなります。
パスワードの変更
ログイン後には、このコントローラでパスワードの変更を行います。
namespace App\Http\Controllers\Auth;
...
class PasswordController extends Controller
{
public function update(Request $request): RedirectResponse
{
$validated = $request->validateWithBag('updatePassword', [
'current_password' => ['required', 'current_password'],
'password' => ['required', Password::defaults(), 'confirmed'],
]);
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
return back()->with('status', 'password-updated');
}
}
パスワードの登録と同様に、Hash::make()が使われています。
Hash::makeは必要ない?
Laravel10以降に装備されたモデルの機能にhashedのキャスティングがあります。以下のUserのモデルで使用されています。
namespace App\Models;
...
#[Fillable(['name', 'email', 'password'])]
#[Hidden(['password', 'remember_token'])]
class User extends Authenticatable
{
/** @use HasFactory<UserFactory> */
use HasFactory, Notifiable;
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}
この設定があると、いちいちHash::make()のコールが要りません。例えば、パスワードの編集では、以下のように変更ができます。
$request->user()->update([
'password' => $validated['password'],
]);
これは便利ですね。しかし、不思議なのはもともとの以下でも2重にハッシュされていないかったことです。
$request->user()->update([
'password' => Hash::make($validated['password']),
]);
順番としては、Hash::makeがコールされてパスワードがハッシュ化されます。その後モデルのハッシュのキャスティングがさらにその値をハッシュ、となりそうですが、そこにおいてすでに値がハッシュ化されているならさらにのハッシュ化はされません。よく考えられているようです。breezeのスターターキットでは両方が使用されていますが、混乱しないためにHash::makeを使わずに、ハッシュのキャスティングだけとした方が良いですね。