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

Дата оновлення перекладу 2022-12-22

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

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

Цей метод аутентифікації може допомогти вам позбутися більшої частини підтримки користувачів, повʼязаної з аутентифікацією (наприклад, я забув свій пароль, як я можу його змінити або скинути і т.д.)

Використання аутентифікатора посилання входу у систему

Цей посібник припускає, що ви налаштували безпеку і створили обʼєкт користувача у своєму додатку. Дотримуйтесь головного посібника з безпеки, якщо ви цього ще не робили.

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!' // субʼєкт електронної пошти
            );
            // створити отримувача для цього користувача
            $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

Tip

Ви також можете налаштувати життєвий цикл для кожного посилання окремо .

Інвалідація посилань входу у систему

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/ вручну може видалити кеш, якщо компонент 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
27
// src/Controller/SecurityController.php
namespace App\Controller;

// 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.