Статьи

Более простая аутентификация с помощью Guard в Symfony 3

Система безопасности Symfony2 — это сложная часть фреймворка, которая сложна для понимания и работы для многих людей. Он очень мощный и гибкий, но не самый простой. В качестве примера пользовательской аутентификации, посмотрите мою предыдущую статью, в которой Symfony интегрирован со службой UserApp.

Блокировка изображения

С выпуском версии 2.8 (и долгожданной версии 3) в инфраструктуру Symfony был принят новый компонент: Guard . Целью этого компонента является интеграция с системой безопасности и предоставление очень простого способа создания пользовательских аутентификаций. Он предоставляет единый интерфейс, методы которого переносят вас от начала до конца цепочки аутентификации: логический и все сгруппированные вместе.

В этой статье мы собираемся создать простую аутентификацию формы, которая требует, чтобы пользователь вошел в систему и имел роль ROLE_ADMIN для каждой страницы. Оригинальный способ создания аутентификации формы все еще может использоваться, но мы будем использовать Guard, чтобы проиллюстрировать его простоту. Затем вы можете применить ту же концепцию к любому виду аутентификации (токен, социальные сети и т. Д.).

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

Конфигурация безопасности

Любая конфигурация безопасности потребует класса User (для представления пользовательских данных) и UserProvider (для получения пользовательских данных). Для простоты мы пойдем с InMemory пользователей InMemory который, в свою очередь, использует класс User Symfony по умолчанию. Итак, наш файл security.yml может начинаться так:

 security: providers: in_memory: memory: users: admin: password: admin roles: 'ROLE_ADMIN' 

Для получения дополнительной информации о системе безопасности Symfony и о том, что может содержать этот файл, я настоятельно рекомендую прочитать запись книги на веб-сайте Symfony.

У нашего провайдера InMemory теперь есть один жестко закодированный тестовый пользователь с ROLE_ADMIN .

Под ключом firewalls мы можем определить наш брандмауэр:

  secured_area anonymous: ~ logout: path: /logout target: / guard: authenticators: - form_authenticator 

В основном это говорит о том, что анонимные пользователи могут получить доступ к брандмауэру и что путь для выхода из системы — /logout Новая часть — это guard ключ, который указывает, какой аутентификатор используется для конфигурации Guard этого брандмауэра: form_authenticator . Это должно быть имя службы, и через минуту мы увидим, как и где оно определено.

Наконец, в конфигурации безопасности мы можем указать некоторые правила доступа:

  access_control: - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/, roles: ROLE_ADMIN } 

В этом примере мы указываем, что пользователи, которые не вошли в систему, могут получить доступ только к пути /login . Для всех остальных ROLE_ADMIN роль ROLE_ADMIN .

Контроллер входа

Прежде чем перейти к фактическому аутентификатору, давайте посмотрим, что у нас на месте для фактической формы входа и контроллера. Внутри нашего DefaultController у нас есть это действие:

  /** * @Route("/login", name="login") */ public function loginAction(Request $request) { $user = $this->getUser(); if ($user instanceof UserInterface) { return $this->redirectToRoute('homepage'); } /** @var AuthenticationException $exception */ $exception = $this->get('security.authentication_utils') ->getLastAuthenticationError(); return $this->render('default/login.html.twig', [ 'error' => $exception ? $exception->getMessage() : NULL, ]); } 

Определяя маршрут /login , это действие отвечает за показ элементарной формы входа пользователям, которые не вошли в систему. Шаблон Twig для этой формы выглядит примерно так:

 {{ error }} <form action="{{ path('login') }}" method="POST"> <label for="username">Username</label> <input type="text" name="username" class="form-control" id="username" placeholder="Username"> <label for="password">Password</label> <input type="password" name="password" class="form-control" id="password" placeholder="Password"> <button type="submit">Login</button> </form> 

Пока ничего особенного. Просто простая разметка формы прямо в HTML для быстрого создания скаффолдинга, который отправляет обратно по тому же пути /login .

Служба аутентификации гвардии

Мы указали в конфигурации безопасности службу для нашего аутентификатора Guard. Давайте удостоверимся, что мы определяем этот сервис в файле services.yml :

 services: form_authenticator: class: AppBundle\Security\FormAuthenticator arguments: ["@router"] 

Этот сервис ссылается на наш класс FormAuthenticator :

 namespace AppBundle\Security; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\RouterInterface; use Symfony\Component\Security\Core\Exception\CustomUserMessageAuthenticationException; use Symfony\Component\Security\Core\Exception\UsernameNotFoundException; use Symfony\Component\Security\Core\Security; use Symfony\Component\Security\Core\User\InMemoryUserProvider; use Symfony\Component\Security\Core\User\UserInterface; use Symfony\Component\Security\Guard\AbstractGuardAuthenticator; use Symfony\Component\Security\Core\Authentication\Token\TokenInterface; use Symfony\Component\Security\Core\Exception\AuthenticationException; use Symfony\Component\Security\Core\User\UserProviderInterface; class FormAuthenticator extends AbstractGuardAuthenticator { /** * @var \Symfony\Component\Routing\RouterInterface */ private $router; /** * Default message for authentication failure. * * @var string */ private $failMessage = 'Invalid credentials'; /** * Creates a new instance of FormAuthenticator */ public function __construct(RouterInterface $router) { $this->router = $router; } /** * {@inheritdoc} */ public function getCredentials(Request $request) { if ($request->getPathInfo() != '/login' || !$request->isMethod('POST')) { return; } return array( 'username' => $request->request->get('username'), 'password' => $request->request->get('password'), ); } /** * {@inheritdoc} */ public function getUser($credentials, UserProviderInterface $userProvider) { if (!$userProvider instanceof InMemoryUserProvider) { return; } try { return $userProvider->loadUserByUsername($credentials['username']); } catch (UsernameNotFoundException $e) { throw new CustomUserMessageAuthenticationException($this->failMessage); } } /** * {@inheritdoc} */ public function checkCredentials($credentials, UserInterface $user) { if ($user->getPassword() === $credentials['password']) { return true; } throw new CustomUserMessageAuthenticationException($this->failMessage); } /** * {@inheritdoc} */ public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) { $url = $this->router->generate('homepage'); return new RedirectResponse($url); } /** * {@inheritdoc} */ public function onAuthenticationFailure(Request $request, AuthenticationException $exception) { $request->getSession()->set(Security::AUTHENTICATION_ERROR, $exception); $url = $this->router->generate('login'); return new RedirectResponse($url); } /** * {@inheritdoc} */ public function start(Request $request, AuthenticationException $authException = null) { $url = $this->router->generate('login'); return new RedirectResponse($url); } /** * {@inheritdoc} */ public function supportsRememberMe() { return false; } } 

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

Сначала мы помещаем это в папку « Security » нашего пакета. Это просто личный выбор, мы не обязаны ему. Затем мы расширяемся от AbstractGuardAuthenticator потому что он уже заботится о реализации необходимого метода из интерфейса GuardAuthenticatorInterface . Если бы нам нужен был определенный класс токенов для представления нашей аутентификации, мы могли бы просто реализовать интерфейс и его createAuthenticatedToken() . На данный момент нам не нужно.

Реализация методов интерфейса — это суть вопроса, и вместе они образуют конвейер аутентификации с момента, когда пользователь пытается получить доступ к сайту, к которому ему предоставлен или запрещен доступ.

Мы начнем с getCredentials() который getCredentials() при каждом запросе. Цель этого метода — либо вернуть данные учетных данных из запроса, либо NULL (либо запрещает доступ, позволяет другому аутентификатору затем предоставлять учетные данные, либо вызывает метод start() ). Поскольку только POST-запросы к нашему пути /login являются контейнерами учетных данных, мы возвращаем массив с предоставленными именем пользователя и паролем, только если это так.

Следующий метод, который getCredentials() если getCredentials() не возвращает NULL, — это getUser() . Последний отвечает за загрузку пользователя на основе учетных данных, которые мы получаем из первого метода. Используя провайдера пользователя по умолчанию (в нашем случае провайдера InMemory ), мы загружаем и возвращаем пользователя на основании его имени пользователя. Несмотря на то, что мы также можем вернуть NULL, чтобы вызвать сбой аутентификации, мы можем выбрать CustomUserMessageAuthenticationException чтобы указать наше собственное сообщение о сбое.

Однако если пользователь извлекается и возвращается, checkCredentials() метод checkCredentials() . Как и ожидалось, целью этого метода является проверка соответствия переданных учетных данных тем, которые были найдены пользователем. И те же правила применяются, если мы возвращаем NULL или выбрасываем исключение, мы не выполняем аутентификацию

На этом этапе пользователь входит в систему, если учетные данные совпадают. В этом случае onAuthenticationSuccess() метод onAuthenticationSuccess() и здесь мы снова можем делать то, что хотим. В нашем случае перенаправление на домашнюю страницу кажется достаточно хорошим примером. Напротив, если аутентификация не удалась, onAuthenticationFailure() метод onAuthenticationFailure() . Мы перенаправляем обратно на страницу /login но не раньше, чем устанавливаем последнее исключение аутентификации в сеансе, чтобы мы могли отобразить сообщение об ошибке над формой. Этот метод вызывается в любой из точек сбоя аутентификации конвейера.

Метод start() является точкой входа в систему Guard (и приложение). Этот метод вызывается всякий раз, когда пользователь пытается получить доступ к странице, требующей аутентификации, но getCredentials() не возвращает учетные данные . В нашем случае это означает, что если кто-то пытается получить доступ к домашней странице, в запросе нет учетных данных, поэтому getCredentials() возвращает NULL. Тогда мы хотим перенаправить на страницу /login чтобы пользователь мог войти на домашнюю страницу.

Давайте представим другой пример: аутентификация на основе токенов. В таком случае каждый запрос должен содержать токен, который аутентифицирует пользователя. Это означает, что getCredentials() всегда должен будет возвращать учетные данные. Если этого не произойдет, метод start() вернет ответ, указывающий, что доступ запрещен (или что бы вы ни думали).

Последний метод отвечает за маркировку функциональности RememberMe . В нашем случае мы не используем его, поэтому возвращаем false. Для получения дополнительной информации о « Помни меня в Symfony» ознакомьтесь с записью в поваренной книге.

Вывод

Теперь у нас есть полностью функционирующая система входа в систему с использованием компонента Guard. Упомянув выше пример аутентификации токена, мы могли бы реализовать его и заставить его работать в тандеме с тем, который мы написали в этой статье. Несколько аутентификаторов могут существовать так:

  guard: authenticators: - form_authenticator - token_authenticator entry_point: form_authenticator 

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

Обратите внимание, что Guard не заменяет ничего, что существует в Symfony, но добавляет к нему. Таким образом, существующие установки безопасности должны продолжать работать. Например, form_login или simple_form как мы его использовали ранее , продолжат работать.

Вы уже попробовали Guard? Как вы к этому относитесь? Дайте нам знать!