Как использовать беспарольную аутентификацию ссылки входа в систему

Дата обновления перевода 2023-01-11

Как использовать беспарольную аутентификацию ссылки входа в систему

Ссылки входа в систему, так же называемые "волшебными ссылками" - это механизм беспарольной аутентификации. Каждый раз, когда пользователь хочет выполнить вход, генерируется новая ссылка, и отправляется ему (например, через электронную почту). Ссылка полностью аутентифицирует пользователя в приложении, если на нее нажать.

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

Использование аутентификатора ссылки входа в систему

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

1) Сконфигурируйте аутентификатор ссылки входа в систему

Аутентификатор ссылки входа в систему конфигурируется с использованием опции login_link под брандмауэром. Вы должны сконфигурировать check_route и signature_properties при подключении этого аутентификатора:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                signature_properties: ['id']

signature_properties используются для создания подписанного URL. Они должены содержать как минимум одно свойство вашего объекта User, которое уникально идентифицирует этого пользователя (к примеру, ID пользователя). Прочтите больше об этой настройке ниже .

check_route должен быть существующим маршрутом, и он будет использован, чтобы сгенерировать ссылку входа в систему, которая аутентифицирует пользователя. Вам не нужен контроллер (или он может быть пустым), так как аутентификатор ссылки входа в систему будет отводить запросы о этому маршруту:

  • Attributes
  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// src/Controller/SecurityController.php
namespace App\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Annotation\Route;

class SecurityController extends AbstractController
{
    #[Route('/login_check', name: 'login_check')]
    public function check()
    {
        throw new \LogicException('This code should never be reached');
    }
}

2) Сгенерируйте ссылку входа в систему

Теперь, когда аутентификатор может проверить ссылки входа в систему, вы должны создать страницу, где пользователь может запросить ссылку входа, и выполнить вход на ваш веб-сайт.

Ссылка входа в систему может быть сгенерирована с использованием LoginLinkHandlerInterface. Правильный обработчик ссылок входа автомонтируется для вас при вводе подсказки в интерфейс:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// src/Controller/SecurityController.php
namespace App\Controller;

use App\Repository\UserRepository;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'login')]
    public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request)
    {
        // проверить, отправлена ли форма входа
        if ($request->isMethod('POST')) {
            // загрузить пользователя каким-то образом (например, используя форму ввода)
            $email = $request->request->get('email');
            $user = $userRepository->findOneBy(['email' => $email]);

            // создать ссылку входа для $user, что вернет экземпляр
            // LoginLinkDetails
            $loginLinkDetails = $loginLinkHandler->createLoginLink($user);
            $loginLink = $loginLinkDetails->getUrl();

            // ... отправить ссылку и вернуть ответ (см. следующий раздел)
        }

        // если это не будет отправлено, отобразить форму "login"
        return $this->render('security/login.html.twig');
    }

    // ...
}
1
2
3
4
5
6
7
8
9
{# templates/security/login.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
<form action="{{ path('login') }}" method="POST">
    <input type="email" name="email">
    <button type="submit">Send Login Link</button>
</form>
{% endblock %}

В этом контроллере, пользователь отправляет свой адрес электронной почты контроллеру. Основываясь на этом свойстве, загружается правильный пользователь, а ссылка входа в систему создается с использованием createLoginLink().

Caution

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

3) Отправьте ссылку входа в систему пользователю

Теперь, когда ссылка создана, ее нужно отправить пользователю. Кто угодно со ссылкой сможет выполнить вход как этот пользователь, поэтому вам нужно убедиться, что вы отправляете ее на известное устройство пользователя (например, используя электронную почту или SMS).

Вы можете отправить ссылку используя любую библиотеку или метод. Однако, аутентификатор ссылки входа в систему предоставляет интеграцию с компонентом Notifier. Используйте специальный LoginLinkNotification, чтобы создать уведомление, и отправить его на электронную почту пользователя или по его номеру телефона:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// src/Controller/SecurityController.php

// ...
use Symfony\Component\Notifier\NotifierInterface;
use Symfony\Component\Notifier\Recipient\Recipient;
use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'login')]
    public function requestLoginLink(NotifierInterface $notifier, LoginLinkHandlerInterface $loginLinkHandler, UserRepository $userRepository, Request $request)
    {
        if ($request->isMethod('POST')) {
            $email = $request->request->get('email');
            $user = $userRepository->findOneBy(['email' => $email]);

            $loginLinkDetails = $loginLinkHandler->createLoginLink($user);

            // создайте уведомление, основанное на деталях ссылки входа в систему
            $notification = new LoginLinkNotification(
                $loginLinkDetails,
                'Welcome to MY WEBSITE!' // email subject
            );
            // создайте получателя для этого пользователя
            $recipient = new Recipient($user->getEmail());

            // отправьте уведомление пользователю
            $notifier->send($notification, $recipient);

            // отобразите страницу "Ссылка входа в систему отправлена!"
            return $this->render('security/login_link_sent.html.twig');
        }

        return $this->render('security/login.html.twig');
    }

    // ...
}

Note

Эта интеграция требует установки и конфигурации компонентов Notifier и Mailer. Установите все необходимые пакеты, используя:

1
2
3
$ composer require symfony/mailer symfony/notifier \
    symfony/twig-bundle twig/extra-bundle \
    twig/cssinliner-extra twig/inky-extra

Это отправит письмо, вроде такого, пользователю:

Tip

Вы можете настроить шаблон этого письма, расширив LoginLinkNotification и сконфигурировав другой htmlTemplate:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Notifier/CustomLoginLinkNotification
namespace App\Notifier;

use Symfony\Component\Security\Http\LoginLink\LoginLinkNotification;

class CustomLoginLinkNotification extends LoginLinkNotification
{
    public function asEmailMessage(EmailRecipientInterface $recipient, string $transport = null): ?EmailMessage
    {
        $emailMessage = parent::asEmailMessage($recipient, $transport);

        // получить объект NotificationEmail и переопределить шаблон
        $email = $emailMessage->getMessage();
        $email->htmlTemplate('emails/custom_login_link_email.html.twig');

        return $emailMessage;
    }
}

Затем, используйте этот новый CustomLoginLinkNotification в контроллере.

Важные рекоммендации

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

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

Ограничение жизненного цикла ссылки входа в систему

Для ссылок входа в систему важно иметь ограниченный жизненный цикл. Это уменьшает риск того, что кто-то может перехватить ссылку, и использовать ее для входа в систему под чужим именем. По умолчанию, Symfony определяет жизненный цикл в 10 минут (600 секунд). Вы можете настроить это, используя опцию lifetime:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                # жизненный цикл в секундах
                lifetime: 300

Инвалидация ссылок входа в систему

Symfony использует подписанные URL для реализации ссылок входа в систему. Премуществом этого является то, что валидные ссылки не должны храниться в БД. Подписанные URL все еще позволяют Symfony инвалидировать уже отправленные ссылки входа в систему, при изменении важной информации (например, адреса электронной почты пользователя).

Подписанный URL содержит 3 параметра:

expires
Временная отметка UNIX, когда истекает срок действия ссылки.
user
Значение, возвращенное из $user->getUserIdentifier() для этого пользователя.
hash
Хеш expires, user и любого сконфигурированного свойства подписи. Каждый раз, когда они изменяются, хеш изменяется и предыдущие ссылки входа инвалидируются.

Вы можете добавить больше свойств к hash, используя опцию signature_properties:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                signature_properties: [id, email]

Свойства извлекаются из объекта пользователя, используя компонент PropertyAccess (например, используя getEmail() или публичное свойство $email в этом примере).

Tip

Вы также можете использовать свойства подписи, чтобы добавить очень продвинутую логику инвалидации в ваши ссылки входа. Например, если вы храните свойство $lastLinkRequestedAt в ваших пользователях, которое вы обновляете в контроллере requestLoginLink(), вы можете инвалидировать все ссылки входа каждый раз, когда пользователь запрашивает новую ссылку.

Сконфигурируйте максимально допустимое использование ссылки

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

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
10
11
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                # позволять использовать ссылку только 3 раза
                max_uses: 3

                # по желанию, сконфигурировать пул кеша
                #used_link_cache: 'cache.redis'

Убедитесь, что в кеше осталось достаточно места, иначе невалидные ссылки не смогут больше сохраняться (и таким образом снова станут валидными). Невалидные ссылки с истекшим сроком годности, автоматически удаляются из кеша.

Пулы кеша не очищаются командой cache:clear, но удаление var/cache/ вручную, может удалить кеш, если компонент кеша сконфигурирован для хранения своего кеша в этом месте. Прочтите руководство Кеш, чтобы узнать больше.

Разрешение единоразового использования ссылки

При установке max_uses в значении 1, вы должны быть особо внимательны, чтобы все работало так, как ожидается. Поставщики электронной почти и браузеры часто загружают предварительный просмотр ссылок, что означает, что ссылка уже инвалидируется загрузчиком предварительного просмотра.

Для того, чтобы решить эту проблему, для начала, установите опцию check_post_only, позвольте аутентификатору только обрабатывать методы HTTP POST:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                check_post_only: true
                max_uses: 1

Затем, используйте контроллер check_route, чтобы отобразить страницу, которая позволяет пользователю создать этот запрос POST (например, нажав на кнопку):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// src/Controller/SecurityController.php
namespace App\Controller;

// ...
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

class SecurityController extends AbstractController
{
    /**
     * @Route("/login_check", name="login_check")
     */
    public function check(Request $request)
    {
        // получить параметры запроса ссылки входа в систему
        $expires = $request->query->get('expires');
        $username = $request->query->get('user');
        $hash = $request->query->get('hash');

        // и отобразить шаблон с кнопкой
        return $this->render('security/process_login_link.html.twig', [
            'expires' => $expires,
            'user' => $username,
            'hash' => $hash,
        ]);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{# templates/security/process_login_link.html.twig #}
{% extends 'base.html.twig' %}

{% block body %}
    <h2>Hi! You are about to login to ...</h2>

    <!-- например, использовать форму со скрытыми полями, чтобы
         создать запрос POST --->
    <form action="{{ path('login_check') }}" method="POST">
        <input type="hidden" name="expires" value="{{ expires }}">
        <input type="hidden" name="user" value="{{ user }}">
        <input type="hidden" name="hash" value="{{ hash }}">

        <button type="submit">Continue</button>
    </form>
{% endblock %}

Настройка обработчика успеха

Иногда, обработка успеха по умолчанию не подходит для вашего случая применения (например, когда вам нужно сгенерировать и вернуть API-ключ). Чтобы настроить то, как ведет себя обработчик успеха, создайте собственный обработчик в виде класса, реализующего AuthenticationSuccessHandlerInterface:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// src/Security/Authentication/AuthenticationSuccessHandler.php
namespace App\Security\Authentication;

use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Http\Authentication\AuthenticationSuccessHandlerInterface;

class AuthenticationSuccessHandler implements AuthenticationSuccessHandlerInterface
{
    public function onAuthenticationSuccess(Request $request, TokenInterface $token): JsonResponse
    {
        $user = $token->getUser();
        $userApiToken = $user->getApiToken();

        return new JsonResponse(['apiToken' => $userApiToken]);
    }
}

Затем, сконфигурируйте этот ID сервиса, как success_handler:

  • YAML
  • XML
  • PHP
1
2
3
4
5
6
7
8
9
# config/packages/security.yaml
security:
    firewalls:
        main:
            login_link:
                check_route: login_check
                lifetime: 600
                max_uses: 1
                success_handler: App\Security\Authentication\AuthenticationSuccessHandler

Tip

Если вы хотите настроить обработку неудач по умолчанию, используйте опцию failure_handler, и создайте класс, реализующий AuthenticationFailureHandlerInterface.

Настройка ссылки входа в систему

Метод createLoginLink() принимает второй необязательный аргумент, чтобы передать объект Request, используемый при генерировании ссылки входа. Это позволяет настраивать функции, вроде локали, используемой для генерирования ссылки:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// src/Controller/SecurityController.php
namespace App\Controller;

// ...
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Http\LoginLink\LoginLinkHandlerInterface;

class SecurityController extends AbstractController
{
    #[Route('/login', name: 'login')]
    public function requestLoginLink(LoginLinkHandlerInterface $loginLinkHandler, Request $request)
    {
        // проверить, отправлена ли форма входа в систему
        if ($request->isMethod('POST')) {
            // ... загрузить пользователя каким-либо образом

            // клонировать и настроить Запрос
            $userRequest = clone $request;
            $userRequest->setLocale($user->getLocale() ?? $request->getDefaultLocale());

            // создать ссылку входа для $user (возвращает экземпляр LoginLinkDetails)
            $loginLinkDetails = $loginLinkHandler->createLoginLink($user, $userRequest);
            $loginLink = $loginLinkDetails->getUrl();

            // ...
        }

        return $this->render('security/login.html.twig');
    }

    // ...
}

По умолчанию, сгенерированные ссылки ипользуют время жизни, сконфигурированное глобально , но вы можете изменить время жизни для ссылки, используя третий аргумент метода createLoginLink():

1
2
3
// третий необяазательный аргумент - это время жизни в секундах
$loginLinkDetails = $loginLinkHandler->createLoginLink($user, null, 60);
$loginLink = $loginLinkDetails->getUrl();

6.2

Аргумент для настройки времени жизни ссылки был представлена в Symfony 6.2.