Авторизация и защищенный раздел на Symfony и PHP

В разделе Плюс+ я выкладываю курс по Symfony и PHP и там я рассказываю от и до, а здесь поднят вопрос – как сделать админку так, чтобы обращения к защищенной области сайта происходил редирект на страницу входа. Это можно использовать для администраторских панелей или для других задач, когда нужно иметь защищенные области сайта.

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

В папке, где вы храните модели создаем файл AdminAuthenticatedInterface.php. У меня модели хранятся в src/Model, более подробно о бизнес моделях я писал здесь: Бизнес логика в Symfony проектах.

В самом файле нам нужен просто интерфейс, у которого совершенно ничего не будет, потому что нам главное имя:

<?php
namespace App\Model;
 
interface AdminAuthenticatedInterface
{
}

Теперь мы будем делать защиту на основе этого интерфейса – если контроллер реализует его, то методы контроллера являются защищенными и должны быть доступны только авторизованным пользователям – администраторам. Если контроллер не реализует, то доступ открыт.

Продолжим считать, что мы создаем раздел администратора и для него вы создаете контроллер AdminController:

class AdminController extends Controller implements AdminAuthenticatedInterface

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

Теперь по идее к этому контроллеру доступ может получить только администратор, но пока только по идее, потому что еще нет такого кода, который бы реально реализовывал задуманную логику. Опять же в модели создаем следующий файл TokenListener.php:

<?php
 
// src/EventListener/TokenListener.php
namespace App\EventListener;
 
use App\Model\AdminAuthenticatedInterface;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\HttpKernel\Event\FilterControllerEvent;
use Symfony\Component\HttpKernel\Event\FilterResponseEvent;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Session\Session;
 
class TokenListener
{
    public function __construct()
    {
    }
 
    public function onKernelController(FilterControllerEvent $event)
    {
        $controller = $event->getController();
 
        if (!is_array($controller)) {
            return;
        }
        if ($controller[0] instanceof AdminAuthenticatedInterface &&
            (new Session)->get('superadmin', '') == '') {
            $event->getRequest()->attributes->set('auth_failed', 'fail');
        }
    }
 
    public function onKernelResponse(FilterResponseEvent $event)
    {
        if ($event->getRequest()->attributes->get('auth_failed') == 'fail') {
            $response = new RedirectResponse('/admin/index');
            $event->setResponse($response);
        }
    }
}

Идея идеи заключается в том, что мы создали класс TokenListener, который будет выполняться при обработке запросов, а точнее его метод onKernelController будет отрабатываться на запросы. Самое интересное здесь это вот эти строки, которые проверяют – является ли текущий контроллер реализацией интерфейса AdminAuthenticatedInterface. Если да, то это защищенная область и нужно убедиться, что текущий пользователь может видеть этот контроллер:

if ($controller[0] instanceof AdminAuthenticatedInterface &&
    (new Session)->get('superadmin', '') == '') {
    $event->getRequest()->attributes->set('admin_auth_failed', 'fail');
}

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

Простейший вариант решения этой проблемы - выставляем флаг в атрибутах запроса, что с запросом проблема и пользователь реально не имеет права доступа к текущему функционалу, именно это происходит в этой строке:

$event->getRequest()->attributes->set('auth_failed', 'fail');

В вот теперь в методе onKernelResponse проверяем – если атрибут auth_failed установлен, то необходимо перенаправить на страницу ввода пароля, а это уже можно сделать в методе, который выполняется при обработке ответа в фреймворке Symfony. На это событие я зарегистрирую чуть позже вот этот метод:

public function onKernelResponse(FilterResponseEvent $event)
{
   if ($event->getRequest()->attributes->get('auth_failed') == 'fail') {
       $response = new RedirectResponse('/login/index');
       $event->setResponse($response);
   }
}

Здесь мы проверяем, установлен ли атрибут ошибки доступа auth_failed. Если он установлен, то доступ к текущему контроллеру был запрещен, и пользователь не авторизован (или не имеет соответствующих прав) и его нужно перенаправить на страницу /login/index.

То, что мы создали этот файл, еще не значит, что он начнет сразу работать. Этот сервис нужно зарегистрировать в файле config/service.yaml, и именно эта настройка потерялась, когда я писал о том, что забыл включить авторизацию https://www.flenov.info/blog/show/Zabyl-podklyuchity-avtorizaciyu. То есть весь код был, но без регистрации в service класс TokenListener реально не будет выполняться при выполнении каждого запроса.

Итак, открываем файл и где-то в конце раздела services: (у меня это в конце файла) нужно добавить следующие строки:

app.tokens.action_listener:
    class: App\Model\TokenListener
    tags:
        - { name: kernel.event_listener, event: kernel.controller, method: onKernelController }
        - { name: kernel.event_listener, event: kernel.response, method: onKernelResponse }

Вот здесь как раз реально и происходит привязка событий kernel.controller методу onKernelController и события kernel.response к методу onKernelResponse.

<?php
namespace App\Controller;
 
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Session\Session;
use Symfony\Component\Routing\Annotation\Route;
use App\Model\AdminAuthenticatedInterface;
 
class AdminloginController extends Controller
{
    /**
     * @Route("/login/index", name="loginindex", methods={"GET"})
     */
    public function indexAction()
    {
        return $this->render(login/index.html.twig', array('email' => '', 'validation' => ''));
  }
 
  /**
     * @Route("/login/index", name="loginpost", methods={"POST"})
     */
    public function loginAction(Request $request)
    {
    if ($request->request->get('email') == "flenov@mail.ru" && 
        $request->request->get('password') == "password") {
           $session = new Session();
           $session->set('superadmin', '1');
           return $this->redirect('/admin/index');
    }
 
    return $this->render('login/index.html.twig',
        array('email' => $request->request->get('email'),
        'validation' => 'Sorry, something is wrong'));
  }
}

Самое главное тут – это строка:

$session->set('superadmin', '1');

Которая выполняется после того, как мы проверили имя и пароль. Если имя и пароль соответствуют администратору, то мы сохраняем в сессии соответствующих флаг. Этот контроллер чисто набросок для того, чтобы показать возможный вариант использования подхода.

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



Внимание!!! Если ты копируешь эту статью себе на сайт, то оставляй ссылку непосредственно на эту страницу. Спасибо за понимание

Комментарии

Паника, что-то случилось!!! Ничего не найдено в комментариях. Срочно нужно что-то добавить, чтобы это место не оставалось пустым.

Добавить Комментарий

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

Без проблем вступаю в неразборчивые разговоры по e-mail. Стараюсь отвечать на письма всех читателей вне зависимости от страны проживания, вероисповедания, на русском или английском языке.

Пишите мне