Автоматическое хэширование значений моделей кастом "Hashed"

Источник: «Automatically Hash Laravel Model Values Using the "Hashed" Cast»
Узнайте, как автоматически хэшировать конфиденциальные данные (например, пароли) с помощью каста "hashed" модели Laravel. Также рассмотрим, как проверить правильность хэширования поля.

Оглавление

Введение

Хэширование — это важная концепция безопасности, о которой должен знать каждый веб-разработчик. Именно оно позволяет надёжно хранить пароли в базах данных.

В этой статье я не буду рассказывать, что такое хэширование, поэтому если вы не знакомы с ним, вам будет интересно ознакомиться с моей статьёй Руководство по шифрованию и хэшированию в Laravel.

В этой статье мы рассмотрим, как автоматически хэшировать значения моделей в проектах Laravel перед сохранением их в базе данных.

Ручное хэширование значений модели

Обычно вы привыкли делать что-то подобное в коде Laravel, чтобы вручную хэшировать значение:

use App\Models\User;
use Illuminate\Support\Facades\Hash;

$user = User::create([
'name' => 'Ash',
'email' => 'mail@ashllendesign.co.uk',
'password' => Hash::make('password'),
]);

В результате выполнения приведённого выше кода поле password для нового пользователя будет храниться в базе данных в таком виде:

$2y$10$Ugrfp6Myf9zVOo66KEuF9uQZ3hyg3T5GhJNgjOTy7o7AXCXSpwWpy

Автоматическое хэширование значений модели

Однако что, если мы хотим автоматически хэшировать поле password для модели User без необходимости каждый раз хэшировать его вручную?

Для этого мы можем использовать каст модели hashed, доступный в Laravel и добавленный в Laravel v10.10.0. Он автоматически хэширует значение поля перед его сохранением в базе данных.

Представим, что мы хотим обновить приведённый выше пример кода и убрать ручное хэширование поля password. Сначала нужно указать в классе App\Models\User, что мы хотим использовать каст модели hashed для поля password. Мы можем сделать это, обновив свойство casts в модели следующим образом:

declare(strict_types=1);

namespace App\Models;

use Illuminate\Foundation\Auth\User as Authenticatable;

final class User extends Authenticatable
{
// ...

protected $casts = [
'password'=> 'hashed',
];
}

Теперь, когда мы создаём нового пользователя, мы можем удалить ручное хэширование поля password следующим образом:

use App\Models\User;

$user = User::create([
'name' => 'Ash',
'email' => 'mail@ashallendesign.co.uk',
'password' => 'password',
]);

Стоит отметить, что каст hashed не будет хэшировать значение, если оно уже было хэшировано. Поэтому, если вы используете Hash::make для ручного хэширования значения, а также каст hashed, вам не нужно беспокоиться, что значение будет хэшировано дважды.

Какой подход следует использовать

Преимущество ручного хэширования значения заключается в том, что значение хэшируется более явно и очевидно. Это означает, что другим разработчикам может быть проще понять, что происходит в коде с первого взгляда. В то время как при использовании каста модели hashed не так очевидно, что значение хэшируется, если разработчик не знает о касте модели.

С другой стороны, преимущество использования каста модели hashed заключается в том, что нужно писать меньше кода. Хотя, на мой взгляд, это преимущество минимально, потому что на самом деле вы удаляете не так уж много кода.

Очень важно помнить, что касты применяются только тогда, когда вы используете модели для создания или обновления записей в базе данных. Например, если вы используете что-то вроде DB-фасада для создания пользователя, его пароль не будет хэширован с помощью каста hashed. В этом случае вам нужно будет не забыть вручную хэшировать значение.

На мой взгляд, оба подхода вполне подходят для использования, у них есть свои плюсы и минусы, и всё зависит от личных предпочтений. Я настоятельно рекомендую использовать только один подход в вашем проекте, чтобы избежать путаницы и быть последовательным. Это уменьшит вероятность того, что кто-то предположит, что значение хэшируется вручную, когда это не так.

Тестирование хэширования значения

Независимо от того, какой подход вы выберете, удобно иметь тест, чтобы убедиться, что значение хэшируется правильно.

Это может быть особенно полезно в ситуациях, когда вы можете случайно предположить, что значение автоматически хэшируется, но это не так.

Итак, давайте рассмотрим базовый тест, который можно написать, чтобы убедиться, что значение хэшируется при сохранении в базе данных.

Представим, что у нас есть базовый класс App\Services\UserService, который мы используем для создания новых пользователей. Класс UserService имеет метод createUser, принимающий воображаемый класс NewUserData. Класс NewUserData имеет свойство password, которое мы хотим проверить на хэширование перед сохранением в базе данных.

Класс UserService может выглядеть примерно так:

//app/Services/UserService.php
declare(strict_types=1);

namespace App\Services;

use App\DataTransferObjects\User;
use App\Models\User;
use Illuminate\Support\Facades\Hash;

final readonly class UserService
{
public function createUser(NewUserData $userData): User
{
return User::create([
'name' => $userData->name,
'email' => $userData->email,
'password' => $userData->password,
]);
}
}

Мы можем написать тест следующим образом:

//tests/Feature/Service/UserService/CreateUserTest.php
declare(strict_types=1);

namespace Tests\Feature\Services\UserService;

use App\DataObjects\NewUserData;
use App\Models\User;
use App\Services\UserService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Hash;
use PHPUnit\Framework\Attributes\Test;
use Tests\TestCase;

final class CreateUserTest extends TestCase
{
use LazilyRefreshDatabase;

#[Test]
public function user_can_be_stored(): void
{
$newUserData = new NewUserData(
name: 'Ash',
email: 'mail@ashallendesign.co.uk',
password: 'password',
);

$user = (new UserService())->createUser($newUserData);

// Проверяем, что пользователь был сохранён в базе данных.
$this->assertDatabaseHas(User::class, [
'id' => $user->id,
'name' => 'Ash',
'email' => 'mail@ashallendesign.co.uk',
]);

// Проверяем, что пароль был хэширован корректно.
$this->assertTrue(
Hash::check('password', $user->password),
);
}
}

В приведённом выше тесте мы вызываем метод createUser класса UserService и передаём объект NewUserData с некоторыми фиктивными данными. Затем мы проверяем, что пользователь был сохранён в базе данных.

Поскольку мы не можем напрямую сравнить хэшированное значение пароля со значением, которое мы передали с помощью утверждения assertDatabaseHas, мы можем использовать метод Hash::check, чтобы проверить, правильно ли хэшировано значение.

Если значение не будет хэшировано, метод Hash::check вернёт false и тест будет провален. В итоге это означает, что если кто-то случайно удалит хэширование из поля пароля, мы сможем это обнаружить.

Заключение

Надеюсь, эта статья показала вам, как можно автоматически хэшировать поля моделей Laravel.

Если эта статья вам понравилась, буду рад услышать об этом. Аналогично, если у вас есть какие-либо замечания для улучшения будущих статей, я тоже буду рад их услышать.

Дополнительные материалы

Предыдущая Статья

Использование интерфейсов в сторонних пакетах

Следующая Статья

Моя стратегия ветвления/тегирования пакетов