Як використовувати безпарольну аутентифікацію посилання входу у систему
Дата оновлення перекладу 2024-06-03
Як використовувати безпарольну аутентифікацію посилання входу у систему
Посилання входу у систему, також відомі під назвою "магічні посилання" - це механізм безпарольної аутентифікації. Кожний раз, коли користувач хоче виконати вхід, генерується нове посилання і відправляється йому (наприклад, через електронну пошту). Посилання повністю аутентифікує користувача у додатку, якщо на неї натиснути.
Цей метод аутентифікації може допомогти вам позбутися більшої частини підтримки користувачів, повʼязаної з аутентифікацією (наприклад, я забув свій пароль, як я можу його змінити або скинути і т.д.)
Використання аутентифікатора посилання входу у систему
Цей посібник припускає, що ви у налаштували безпеку і створили обʼєкт користувача своєму додатку.
1) Сконфігуруйте аутентифікатор входу у систему
Аутентифікатор посилання входу у систему конфігурується з використанням опції
login_link
під брандмауером. Ви повинні сконфігурувати check_route
та
signature_properties
при підключенні цього аутентифікатора:
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
повинен бути існуючим маршрутом, і він буде використаний, щоб згенерувати
посилання входу у систему, яке аутентифікує користувача. Вам не потрібен контролер (або
він може бути порожнім), так як аутентифікатор посилання входу у систему відводитиме запити
за цим маршрутом:
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\Attribute\Route;
class SecurityController extends AbstractController
{
#[Route('/login_check', name: 'login_check')]
public function check(): never
{
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 35
// 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\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\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): Response
{
// перевірити, чи відправлена форма входу
if ($request->isMethod('POST')) {
// завантажити користувача якимось чином (наприклад, використовуючи форму введення)
$email = $request->getPayload()->get('email');
$user = $userRepository->findOneBy(['email' => $email]);
// створити посилання входу для $user, що поверне екземпляр
// LoginLinkDetails
$loginLinkDetails = $loginLinkHandler->createLoginLink($user);
$loginLink = $loginLinkDetails->getUrl();
// ... відправити посилання та повернути відповідь (див. наступний розділ)
}
// якщо це не буде відправлено, відобразити форму "login"
return $this->render('security/request_login_link.html.twig');
}
// ...
}
1 2 3 4 5 6 7 8 9
{# templates/security/request_login_link.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): Response
{
if ($request->isMethod('POST')) {
$email = $request->getPayload()->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
:
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
і будь-якого сконфігурованої властивості підпису. Кожний раз, коли вони змінюються, хеш змінюється і попередні посилання входу інвалідуються.
Для користувача, який повертає user@example.com
у виклику $user->getUserIdentifier()
?,
згенероване посилання для входу виглядає наступним чином:
1
http://example.com/login_check?user=user@example.com&expires=1675707377&hash=f0Jbda56Y...A5sUCI~TQF701fwJ...7m2n4A~
Ви можете додати більше властивостей до hash
, використовуючи опцію
signature_properties
:
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
:
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:
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
// 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): Response
{
// отримати параметри запиту посилання входу у систему
$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 %}
Стратегія хешування
Внутрішньо, реалізація LoginLinkHandler використовує SignatureHasher для створення хешу, що міститься у посиланні для входу.
Цей хешер створює перший хеш з датою закінчення терміну дії посилання, сконфігурованими значеннями властивостей підпису та ідентифікатором користувача. Використовуваний алгоритм хешування - SHA-256.
Після того, як цей перший хеш обробляється і кодується в Base64, створюється новий хеш
з першого хеш-значення і параметра контейнера kernel.secret
. Це
дозволяє Symfony підписувати цей остаточний хеш, який міститься в URL-адресі для входу.
Остаточний хеш також є хешем SHA-256, закодованим у Base64.
Налаштування обробника успіху
Іноді, обробка успіху за замовчуванням не підходить для вашого випадку використання (наприклад, коли вам потрібно згенерувати та повернути 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
:
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): Response
{
// перевірити, чи відправлена форма входу у систему
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/request_login_link.html.twig');
}
// ...
}
За замовчуванням, згенеровані посилання використовують
життєвий цикл, сконфігурований глобально , але
ви можете змінити життєвий цикл для кожного посилання окремо, використовуючи
третій аргумент методу createLoginLink()
:
1 2 3
// третій необовʼязковий аргумент - це життєвий цикл в секундах
$loginLinkDetails = $loginLinkHandler->createLoginLink($user, null, 60);
$loginLink = $loginLinkDetails->getUrl();