Encodera

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.

Оглавление

Вход пользователей

Аутентификация — проверка того, кто перед нами. Готового входа в Slim нет, но он собирается из уже знакомых частей: таблица пользователей, форма, сессия и посредник, который не пускает гостей на закрытые страницы.

Таблица пользователей

Добавьте в database/schema.sql и выполните в базе:

-- database/schema.sql
CREATE TABLE users (
    id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    email VARCHAR(255) NOT NULL UNIQUE,
    password_hash VARCHAR(255) NOT NULL
);

В базе хранят не пароль, а его хеш — необратимо преобразованную строку. Первого пользователя создайте небольшим скриптом в новой папке bin:

// bin/create-user.php
$app = require __DIR__ . '/../config/bootstrap.php';
 
$pdo = $app->getContainer()->get(PDO::class);
 
$statement = $pdo->prepare('INSERT INTO users (email, password_hash) VALUES (?, ?)');
$statement->execute([$argv[1], password_hash($argv[2], PASSWORD_DEFAULT)]);
 
echo "Пользователь создан\n";
php bin/create-user.php anna@example.com secret-password

Скрипт подключает то же приложение, берёт из контейнера PDO и добавляет строку с хешем пароля.

Поиск пользователя

// src/Repository/UserRepository.php
namespace App\Repository;
 
use PDO;
 
final class UserRepository
{
    public function __construct(private PDO $pdo) {}
 
    public function findByEmail(string $email): ?array
    {
        $statement = $this->pdo->prepare('SELECT * FROM users WHERE email = ?');
        $statement->execute([$email]);
 
        return $statement->fetch() ?: null;
    }
}

Действие входа

Форму входа — шаблон templates/auth/login.twig с полями email и password и действие LoginFormAction — создайте так же, как форму статьи. Данные принимает второе действие:

// src/Action/LoginAction.php
final class LoginAction
{
    public function __construct(private UserRepository $users, private Twig $twig) {}
 
    public function __invoke(Request $request, Response $response): Response
    {
        $data = (array) $request->getParsedBody();
        $user = $this->users->findByEmail(trim($data['email'] ?? ''));
 
        if ($user === null || !password_verify($data['password'] ?? '', $user['password_hash'])) {
            return $this->twig->render($response->withStatus(422), 'auth/login.twig', [
                'error' => 'Неверная почта или пароль',
            ]);
        }
 
        session_regenerate_id(true);
        $_SESSION['user_id'] = (int) $user['id'];
 
        return $response->withHeader('Location', '/posts')->withStatus(302);
    }
}

password_verify() сравнивает введённый пароль с хешем. При успехе номер пользователя записывается в сессию — это и есть «вход». session_regenerate_id() выдаёт сессии новый идентификатор, чтобы старый нельзя было использовать повторно.

Посредник доступа

// src/Middleware/AuthMiddleware.php
final class AuthMiddleware implements MiddlewareInterface
{
    public function __construct(private ResponseFactoryInterface $responseFactory) {}
 
    public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
    {
        if (empty($_SESSION['user_id'])) {
            return $this->responseFactory->createResponse(302)->withHeader('Location', '/login');
        }
 
        return $handler->handle($request->withAttribute('user_id', $_SESSION['user_id']));
    }
}

Гостю посредник сам возвращает перенаправление — действие не выполняется. Объект ответа создаёт фабрика ResponseFactoryInterface, описанная в контейнере.

Маршруты

// config/routes.php
$app->get('/login', LoginFormAction::class)->setName('login');
$app->post('/login', LoginAction::class);
 
$app->group('', function (RouteCollectorProxy $group) {
    $group->get('/posts/new', NewPostAction::class)->setName('posts.new');
    $group->post('/posts', CreatePostAction::class)->setName('posts.store');
})->add(AuthMiddleware::class);

Группа с пустым началом адреса нужна только для того, чтобы привязать посредник к нескольким маршрутам. Откройте /posts/new без входа — браузер перейдёт на /login. После входа форма откроется.

Выход

Действие выхода очищает сессию и перенаправляет на главную:

// src/Action/LogoutAction.php
$_SESSION = [];
session_destroy();
 
return $response->withHeader('Location', '/')->withStatus(302);

Маршрут выхода делают методом POST, чтобы чужая ссылка не могла разлогинить посетителя.

Частая ошибка

Сообщения «такой почты нет» и «пароль неверный» по отдельности подсказывают постороннему, какие адреса зарегистрированы. Показывайте одно общее сообщение для обоих случаев. И никогда не сравнивайте пароли через == и не храните их в открытом виде — только password_hash() и password_verify().

Спасибо, что читаете!

Пожертвуйте сайту, мы стараемся ради вас.