30 June

[Symfony 5] Раздельная авторизация для админов и пользователей с двумя разными сущностями и формами входа

PHPSymfony
Sandbox

Цель


Создать в Symfony 5 раздельную авторизацию:

  • Администратор — будет иметь сущность Admin, url для входа /admin/login
  • Пользователь — будет иметь сущность User, url для входа /login
  • Данные для входа не должны пересекаться, мы не можем авторизоваться как User на странице /admin/login
  • Должны быть созданы две разные Entities
  • Должны быть созданы два разных контроллера для входа и два разных Security
  • Возможность настройки переадресации после авторизации отдельно друг от друга
  • Возможность использовать разные авторизационные данные (например, для User мы хотим, чтобы пользователи вводили email/password, а для Admin обеспечить доп. защиту, добавив какой-нибудь Uuid

Зачем нужен этот гайд?


У меня стояла задача разделить форму входа с сущностью User на две разные — для пользователя (entity User) и для администратора (entity Admin) для нормального функционала админ панели (в данном случае EasyAdmin).

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

Технические характеристики


  • Windows 10
  • OpenServer 5.3.7
  • PHP 7.4
  • MariaDB-10.2.12
  • Symfony 5.1

Туториал актуален на конец июня 2020 года.

Шаг 0 — ставим Symfony 5


Будем считать, что вы поставили все нужные компоненты, включая Composer в корневом каталоге OpenServer (.../domains).

composer create-project symfony/website-skeleton auth_project


Шаг 1 — настраиваем бд


Создаем новую БД, назовём auth_project, пароль и пользователь пусть будет mysql. Теперь нам нужно переопределить настройки .env

Должно быть так:


# In all environments, the following files are loaded if they exist,
# the latter taking precedence over the former:
#
#  * .env                contains default values for the environment variables needed by the app
#  * .env.local          uncommitted file with local overrides
#  * .env.$APP_ENV       committed environment-specific defaults
#  * .env.$APP_ENV.local uncommitted environment-specific overrides
#
# Real environment variables win over .env files.
#
# DO NOT DEFINE PRODUCTION SECRETS IN THIS FILE NOR IN ANY OTHER COMMITTED FILES.
#
# Run "composer dump-env prod" to compile .env files for production use (requires symfony/flex >=1.2).
# https://symfony.com/doc/current/best_practices.html#use-environment-variables-for-infrastructure-configuration

###> symfony/framework-bundle ###
APP_ENV=dev
APP_SECRET=16cbb669c87ff9259c522ee2846cb397
#TRUSTED_PROXIES=127.0.0.0/8,10.0.0.0/8,172.16.0.0/12,192.168.0.0/16
#TRUSTED_HOSTS='^(localhost|example\.com)$'
###< symfony/framework-bundle ###

###> symfony/mailer ###
# MAILER_DSN=smtp://localhost
###< symfony/mailer ###

###> doctrine/doctrine-bundle ###
# Format described at https://www.doctrine-project.org/projects/doctrine-dbal/en/latest/reference/configuration.html#connecting-using-a-url
# For an SQLite database, use: "sqlite:///%kernel.project_dir%/var/data.db"
# For a PostgreSQL database, use: "postgresql://db_user:db_password@127.0.0.1:5432/db_name?serverVersion=11&charset=utf8"
# IMPORTANT: You MUST configure your server version, either here or in config/packages/doctrine.yaml
DATABASE_URL=mysql://mysql:mysql@127.0.0.1:3306/auth_project?serverVersion=mariadb-10.2.12
###< doctrine/doctrine-bundle ###


Шаг 2 — создаем сущность User


Создаем сущность User, в качестве уникального значения выбираем email

php bin/console make:user

image

Шаг 3 — создаем сущность Admin


Повторяем всё то, что описано в предыдущем шаге, вместо названия сущности User ставим Admin

Шаг 4 — подготовим Fixtures


Создадим 2 тестовых аккаунта, один для User, второй для Admin. Воспользуемся DoctrineFixturesBundle

Для начала нужно его поставить

composer require --dev orm-fixtures

После установки в /src появится папка DataFixtures, в котором уже будет создан файл AppFixtures.php

Переименуем его в UserFixtures.php и добавим в него нужный функционал

<?php

namespace App\DataFixtures;

use App\Entity\User;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class UserFixtures extends Fixture
{
    private $encoder;

    private $em;

    public function __construct(UserPasswordEncoderInterface $encoder, EntityManagerInterface $entityManager)
    {
        $this->encoder = $encoder;
        $this->em = $entityManager;
    }

    public function load(\Doctrine\Persistence\ObjectManager $manager)
    {
        $usersData = [
              0 => [
                  'email' => 'user@example.com',
                  'role' => ['ROLE_USER'],
                  'password' => 123654
              ]
        ];

        foreach ($usersData as $user) {
            $newUser = new User();
            $newUser->setEmail($user['email']);
            $newUser->setPassword($this->encoder->encodePassword($newUser, $user['password']));
            $newUser->setRoles($user['role']);
            $this->em->persist($newUser);
        }

        $this->em->flush();
    }
}

Тоже самое необходимо сделать и для админа — создаём AdminFixtures.php

<?php

namespace App\DataFixtures;

use App\Entity\Admin;
use Doctrine\Bundle\FixturesBundle\Fixture;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Security\Core\Encoder\UserPasswordEncoderInterface;

class AdminFixtures extends Fixture
{
    private $encoder;

    private $em;

    public function __construct(UserPasswordEncoderInterface $encoder, EntityManagerInterface $entityManager)
    {
        $this->encoder = $encoder;
        $this->em = $entityManager;
    }

    public function load(\Doctrine\Persistence\ObjectManager $manager)
    {
        $adminsData = [
              0 => [
                  'email' => 'admin@example.com',
                  'role' => ['ROLE_ADMIN'],
                  'password' => 123654
              ]
        ];

        foreach ($adminsData as $admin) {
            $newAdmin = new Admin();
            $newAdmin->setEmail($admin['email']);
            $newAdmin->setPassword($this->encoder->encodePassword($newAdmin, $admin['password']));
            $newAdmin->setRoles($admin['role']);
            $this->em->persist($newAdmin);
        }

        $this->em->flush();
    }
}

Шаг 5 — загружаем миграции и fixtures в бд


Сущности созданы, fixtures мы прописали, осталось теперь всё это залить в бд, следующие действия я провожу при каждом изменении сущностей или fixtures


php bin/console doctrine:schema:drop --full-database --force # очистить базу, удалив все таблицы

php bin/console doctrine:migrations:diff # создать новою миграцию. ПЕРЕД ЭТИМ НУЖНО УДАЛИТЬ ВСЕ СТАРЫЕ МИГРАЦИИ!

php bin/console doctrine:migrations:migrate # отправить новою миграцию в базу
php bin/console doctrine:fixtures:load # загрузить фейковые и реальные данные

Шаг 6 — создаём авторизацию


В консоли прописываем

php bin/console make:auth

Настройки и названия ставим такие:


# php bin/console make:auth

 What style of authentication do you want? [Empty authenticator]:
  [0] Empty authenticator
  [1] Login form authenticator
 > 1

 The class name of the authenticator to create (e.g. AppCustomAuthenticator):
 > UserAuthenticator

 Choose a name for the controller class (e.g. SecurityController) [SecurityController]:
 > UserAuthSecurityController

 Do you want to generate a '/logout' URL? (yes/no) [yes]:
 >

 created: src/Security/UserAuthenticator.php
 updated: config/packages/security.yaml
 created: src/Controller/UserAuthSecurityController.php
 created: templates/security/login.html.twig

  Success!

 Next:
 - Customize your new authenticator.
 - Finish the redirect "TODO" in the App\Security\UserAuthenticator::onAuthenticationSuccess() method.
 - Review & adapt the login template: templates/security/login.html.twig.

В результате у нас обновиться security.yaml и создадутся 3 файла

Шаг 7 — правим security.yaml


После создания авторизации security.yaml выглядит так:


security:
    encoders:
        App\Entity\User:
            algorithm: auto
        App\Entity\Admin:
            algorithm: auto


    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\Admin
                property: email
        # used to reload user from session & other features (e.g. switch_user)
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false
        main:
            anonymous: true
            lazy: true
            provider: app_user_provider
            guard:
                authenticators:
                    - App\Security\UserAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                # target: app_any_route

            # activate different ways to authenticate
            # https://symfony.com/doc/current/security.html#firewalls-authentication

            # https://symfony.com/doc/current/security/impersonating_user.html
            # switch_user: true

    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }


Нам необходимо добавить новый provider admin_user_provider и поменять настройки у firewalls.

Финально, файл security.yaml должен выглядить вот так:


security:
    encoders:
        App\Entity\User:
            algorithm: auto
        App\Entity\Admin:
            algorithm: auto


    # https://symfony.com/doc/current/security.html#where-do-users-come-from-user-providers
    providers:
        # used to reload user from session & other features (e.g. switch_user)
        app_user_provider:
            entity:
                class: App\Entity\User
                property: email
        app_admin_provider:
            entity:
                class: App\Entity\Admin
                property: email
        # used to reload user from session & other features (e.g. switch_user)
    firewalls:
        dev:
            pattern: ^/(_(profiler|wdt)|css|images|js)/
            security: false

        admin_secured_area:
            pattern:   ^/admin
            anonymous: ~
            provider: app_admin_provider
            form_login:
                login_path: /admin/login
                check_path: /admin/login_check
                default_target_path: /admin/login
                username_parameter: email
                password_parameter: password
            guard:
                authenticators:
                    - App\Security\AdminAuthenticator
            logout:
                path: app_logout
                # where to redirect after logout
                target: /admin/login

        user_secured_area:
            pattern:   ^/
            anonymous: ~
            provider: app_user_provider
            form_login:
                login_path: /login
                check_path: /login_check
                default_target_path: /login
                username_parameter: email
                password_parameter: password
            logout:
                path: app_logout
                # where to redirect after logout
                target: /login
                
    # Easy way to control access for large sections of your site
    # Note: Only the *first* access control that matches will be used
    access_control:
        # - { path: ^/admin, roles: ROLE_ADMIN }
        # - { path: ^/profile, roles: ROLE_USER }


Шаг 8 — переименовываем шаблон login.html.twig


Это нужно сделать, так как мы повторно будем создавать авторизацию через make:auth.
Назовем этот файл.

Шаг 9 — редактируем UserAuthSecurityController


Файл находится по пути App\Controller, так как мы поменяли название шаблона, в контроллере это необходимо изменить.

Каким должен быть контроллер:


<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class UserAuthSecurityController extends AbstractController
{
    /**
     * @Route("/login", name="app_login")
     */
    public function login(AuthenticationUtils $authenticationUtils): Response
    {
        // if ($this->getUser()) {
        //     return $this->redirectToRoute('target_path');
        // }

        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/user-login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
    }

    /**
     * @Route("/logout", name="app_logout")
     */
    public function logout()
    {
        throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
    }
}

Шаг 10 — создаём вторую авторизацию


В консоли прописываем:


php bin/console make:auth

Так как мы добавили новый app_admin_provider — нам предложат выбрать какой firewall мы хотим обновить:

image

После выбора firewall предложать выбрать Entity, выбираем \App\Entity\Admin:

image

Шаг 11 — переименовываем только что созданный login.html.twig


Переименовываем только что созданный login.html.twig на admin-login.html.twig

Шаг 12 — редактируем только что созданный AdminAuthController


Меняем route и название шаблона:


<?php

namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\Authentication\AuthenticationUtils;

class AdminAuthController extends AbstractController
{
    /**
     * @Route("/admin/login", name="app_admin_login")
     */
    public function adminLogin(AuthenticationUtils $authenticationUtils): Response
    {
        // if ($this->getUser()) {
        //     return $this->redirectToRoute('target_path');
        // }

        // get the login error if there is one
        $error = $authenticationUtils->getLastAuthenticationError();
        // last username entered by the user
        $lastUsername = $authenticationUtils->getLastUsername();

        return $this->render('security/admin-login.html.twig', ['last_username' => $lastUsername, 'error' => $error]);
    }

    /**
     * @Route("/logout", name="app_logout")
     */
    public function logout()
    {
       throw new \LogicException('This method can be blank - it will be intercepted by the logout key on your firewall.');
    }
}

Шаг 13 — редактируем файл config/routes.yaml


Создаем login_check и admin_login_check, которые мы определили в настройках firewall в файле config/packages/security.yaml

Как должен выглядеть файл config/routes.yaml:


#index:
#    path: /
#    controller: App\Controller\DefaultController::index
login_check:
  path: /login_check
admin_login_check:
  path: /admin/login_check


Шаг 14 — редактируем файл templates/secutiry/user-login.html.twig


Добавляем в тег атрибут action:

{% extends 'base.html.twig' %}

{% block title %}Log in!{% endblock %}

{% block body %}
<form action="{{ path('login_check') }}" method="post">
    {% if error %}
        <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    {% if app.user %}
        <div class="mb-3">
            You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
        </div>
    {% endif %}

    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="inputEmail">Email</label>
    <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" required autofocus>
    <label for="inputPassword">Password</label>
    <input type="password" name="password" id="inputPassword" class="form-control" required>

    <input type="hidden" name="_csrf_token"
           value="{{ csrf_token('authenticate') }}"
    >

    {#
        Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
        See https://symfony.com/doc/current/security/remember_me.html

        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" name="_remember_me"> Remember me
            </label>
        </div>
    #}

    <button class="btn btn-lg btn-primary" type="submit">
        Sign in
    </button>
</form>
{% endblock %}


Шаг 15 — редактируем файл templates/secutiry/admin-login.html.twig


Добавляем в тег атрибут action:

{% extends 'base.html.twig' %}

{% block title %}Log in!{% endblock %}

{% block body %}
<form action="{{ path('admin_login_check') }}" method="post">
    {% if error %}
        <div class="alert alert-danger">{{ error.messageKey|trans(error.messageData, 'security') }}</div>
    {% endif %}

    {% if app.user %}
        <div class="mb-3">
            You are logged in as {{ app.user.username }}, <a href="{{ path('app_logout') }}">Logout</a>
        </div>
    {% endif %}

    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <label for="inputEmail">Email</label>
    <input type="email" value="{{ last_username }}" name="email" id="inputEmail" class="form-control" required autofocus>
    <label for="inputPassword">Password</label>
    <input type="password" name="password" id="inputPassword" class="form-control" required>

    <input type="hidden" name="_csrf_token"
           value="{{ csrf_token('authenticate') }}"
    >

    {#
        Uncomment this section and add a remember_me option below your firewall to activate remember me functionality.
        See https://symfony.com/doc/current/security/remember_me.html

        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" name="_remember_me"> Remember me
            </label>
        </div>
    #}

    <button class="btn btn-lg btn-primary" type="submit">
        Sign in
    </button>
</form>
{% endblock %}


Шаг 16 — запускаем сайт


Для запуска сайта сначала ставим web-server-bundle:

composer require symfony/web-server-bundle --dev ^4.4.2

Запускаем сайт:

php bin/console server:run

Шаг 17 — тестируем авторизацию для User


Переходим на страницу 127.0.0.1:8000/login

Видим это:

image

Авторизовываемся, используя email user@example.com и пароль 123654.

Видим, что авторизация прошла успешно:

image

Если использовать неправильные данные, выбьет ошибку Invalid credentials.

Шаг 18 — тестируем авторизацию для Admin


Переходим на страницу 127.0.0.1:8000/admin/login

Видим это:

image

Авторизовываемся, используя email admin@example.com и пароль 123654.

Видимо, что всё успешно:

image

При вводе неправильных данных или если мы введем данные от User на странице /admin/login — вылетит ошибка, что Invalid credentials. Для страницы /login тоже самое — введем данные от Admin — будет ошибка.

Заключение


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

Написать туториал решил после того, как не нашел подробной инструкции для данной задачи в английской документации, гайдах или обсуждениях, не говоря уже о материалах на русском языке.
Tags:symfony 5авторизацияраздельная авторизациядве формы для входа
Hubs: PHP Symfony
+12
3.1k 23
Comments 11
Popular right now