Статьи

Система предварительной регистрации и приглашения Symfony2

Мы обсуждали разработку Symfony 2 в предыдущих статьях SitePoint и создали клон моего личного приложения Symfony (часть 1 , 2 и 3 ). Но Symfony 2 — гигантский фреймворк, и мы можем охватить еще немало тем.

Symfony-логотип

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

Типичный поток управления пользователями может иметь следующие задачи:

  • Встроенный пользователь будет создан после установки приложения, и ему будет предоставлена ​​привилегия root .
  • Любой новый пользователь может зарегистрироваться через форму или зарегистрироваться только по приглашению (этот подход обсуждается в этой статье).
  • После регистрации пользовательская запись сохраняется в базовой базе данных / таблице.
  • При желании приложение переведет этого нового пользователя в состояние ожидания и отправит подтверждение по электронной почте. Пользователь будет «активирован» только тогда, когда он щелкнет ссылку в электронном письме с токеном подтверждения. Этот подход не используется в этой статье, потому что мы приглашаем пользователей, а сайт является сайтом «замкнутого круга».
  • Пользователь входит в систему. Приложение проверит имя пользователя и пароль.
  • При желании приложение может выполнять некоторые действия после входа в систему. В этом случае мы обновим дату / время последнего входа пользователя в базу данных и перенаправим их.
  • Пользователь может явно выбрать выход.

Основная user таблица

Хотя Symfony поддерживает аутентификацию пользователей в памяти, это не рекомендуется в реальных приложениях. В большинстве случаев мы будем использовать другие ресурсы (базы данных, LDAP и т. Д.), Чтобы сохранить учетные данные пользователя. Мы будем использовать базу данных SQL в нашем приложении.

Сначала давайте создадим таблицу для хранения информации о пользователе:

 CREATE TABLE `user` ( `id` INT( 255 ) AUTO_INCREMENT NOT NULL, `username` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `password` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `email` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `created` DATETIME NOT NULL, `logged` DATETIME NULL, `roles` VARCHAR( 25 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `gravatar` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `active` TINYINT( 1 ) NOT NULL, `homepage` VARCHAR( 255 ) CHARACTER SET utf8 COLLATE utf8_general_ci NULL, PRIMARY KEY ( `id` ) 

Из этих определенных полей username и password используются для предоставления / отклонения запроса на вход в систему. Это называется аутентификацией.

После успешного входа в систему пользователю предоставляется ROLE , которая определяется полем roles . Разные роли будут иметь разные права доступа при посещении URI. Это называется авторизацией.

username , password и roles вместе образуют краеугольные камни нашей системы безопасности.

Конфигурация: security.yml

Symfony использует файл security.yml для хранения всех настроек и настроек, связанных с безопасностью приложения.

Ниже приведено содержимое нашего файла security.yml (находится в app/config ):

 security: providers: administrators: entity: { class: AppBundle:User, property: username } encoders: AppBundle\Entity\User: algorithm: bcrypt cost: 12 firewalls: dev: pattern: ^/(_(profiler|wdt|error)|css|images|js)/ security: false default: anonymous: ~ http_basic: ~ form_login: login_path: /login check_path: /login_check logout: path: /logout target: /login access_control: - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/preregister, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/create, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/invite, roles: [ROLE_ADMIN] } - { path: ^/, roles: [ROLE_USER, ROLE_ADMIN] } участия security: providers: administrators: entity: { class: AppBundle:User, property: username } encoders: AppBundle\Entity\User: algorithm: bcrypt cost: 12 firewalls: dev: pattern: ^/(_(profiler|wdt|error)|css|images|js)/ security: false default: anonymous: ~ http_basic: ~ form_login: login_path: /login check_path: /login_check logout: path: /logout target: /login access_control: - { path: ^/login, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/register, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/preregister, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/create, roles: IS_AUTHENTICATED_ANONYMOUSLY } - { path: ^/invite, roles: [ROLE_ADMIN] } - { path: ^/, roles: [ROLE_USER, ROLE_ADMIN] } 

Под security у нас есть четыре подраздела:

providers : ответственные за предоставление учетных данных пользователей

Мы будем хранить пользовательскую информацию в таблице, отображаемой в Symfony как сущность (класс PHP) с необходимыми средствами получения / установки для управления свойствами (полями таблицы).

В этом случае сущностью является AppBundle:User , который происходит из src/AppBundle/Entity/User.php а свойство, которое мы используем для проверки запроса на вход, исходит от имени username .

Symfony поддерживает использование других полей или одного из нескольких полей в качестве идентификатора пользователя. Распространенным сценарием является возможность входа пользователя с использованием либо имени username либо поля email . Пожалуйста, обратитесь к официальной документации Symfony, если вы хотите узнать больше об этом.

Как кодируется пароль: encoders

Хранение паролей в виде простого текста никогда не является хорошей идеей. В нашем случае мы указываем, что будем использовать метод bcrypt с 12 циклами. Это создаст достаточно сильный хеш при разумных затратах (времени на машинное вычисление — чем больше времени требуется для создания хэша пароля, тем сложнее для успешной атаки методом перебора ).

Функции входа и выхода: firewalls/default

Мы должны указать, как мы должны входить в систему. Symfony может использовать HTTP Basic login (который открывает диалоговое окно для ввода имени пользователя / пароля) или формы входа. В нашем случае мы используем форму:

 form_login: login_path: /login check_path: /login_check 

Конечно, мы даем пользователю возможность явно выйти из системы:

 logout: path: /logout target: /login 

Здесь настройка target сообщает приложению, куда идти (в данном случае обратно на страницу login ) после выхода пользователя из системы.

Для правильной работы вышеуказанных настроек Symfony требуется три маршрута в файле routing.yml :

 logout: path: /logout login: path: /login defaults: { _controller: AppBundle:Security:login} login_check: path: /login_check 

В частности, путь /login должен быть связан с правильным действием в контроллере ( AppBundle:Security:login ). Два других могут быть определены с помощью имени и пути.

access_control

В этом подразделе мы определяем соответствие между шаблонами URI и требуемым уровнем доступа.

В целом у нас есть 3 разных уровня:

  • IS_AUTHENTICATED_ANONYMOUSLY — это значит кого угодно. Контроль доступа не будет применяться при посещении URI с этим правом доступа.
  • ROLE_USER — обычный пользователь, ROLE_USER проверку входа, может получить доступ к этому URI.
  • ROLE_ADMIN — суперпользователь, имеющий некоторые специальные привилегии, может получить доступ к этому URI.

Имена ( ROLE_USER и ROLE_ADMIN ) являются искусственными. Разработчики могут применять свой собственный набор имен, которые будут использоваться в приложении (например, использование USER и ADMIN также хорошо).

Общая ошибка при определении контроля доступа делает контроль слишком строгим и непригодным для использования.

Независимо от того, насколько строгое приложение требует контроля доступа, приложение ДОЛЖНО по крайней мере разрешить анонимный доступ к странице login в login . Как и в случае с маршрутами Symfony, мы ДОЛЖНЫ определить более конкретные правила ранее.

Мы должны освободить доступ ко всем маршрутам, связанным с регистрацией пользователей ( register , preregister , create ) и входом в систему ( login ), чтобы разрешить анонимный доступ. Эти маршруты определены в первую очередь.

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

Наконец, мы «закрываем» весь сайт ( ^/ , за исключением тех шаблонов URI, которые были определены ранее) для всех, кроме «инсайдеров» ( ROLE_USER или ROLE_ADMIN ).

AppBundle:User объект

Я предпочитаю, чтобы AppBundle:User объект создавался путем импорта существующей структуры БД. Этот объект сам по себе еще не может использоваться процессом аутентификации. Нам нужны дальнейшие модификации:

 <?php namespace AppBundle\Entity; use Symfony\Component\Security\Core\User\UserInterface; /** * User */ class User implements UserInterface, \Serializable { /** * @var integer */ private $id; //All the table properties follow... public function getId() { return $this->id; } //All the getters/setters follow... /** * Get roles * * @return [string] */ public function getRoles() { return [$this->roles]; } public function eraseCredentials() { return; } public function serialize() { ... } public function unserialize($serialized) { ... } public function getSalt() { ... } } 

Класс User должен реализовывать два интерфейса: Symfony\Component\Security\Core\User\UserInterface и \Serializable . Это потребует от нас определения дополнительных методов, включая eraseCredentials , serialize , unserialize и getSalt .

Примечание об определении метода getRoles . roles — это поле в нашей user таблице, поэтому метод получения автоматически генерируется следующим образом:

 public function getRoles() { return $this->roles; } 

Это потому, что мы определяем поле roles так же просто, как VARCHAR(255) , который вернет сохраненную нами строку. Однако для Symfony требуются роли в формате массива, поэтому, несмотря на то, что для нашего пользователя на данный момент сохранена только одна строковая роль, мы учитываем систему с несколькими ролями и требование ролей как массив, разбивая эту строку. Таким образом, если мы хотим, чтобы у пользователя было несколько ролей, мы можем сохранить их в виде списка через запятую ( ROLE_ADMIN, ROLE_BLOGGER, ROLE_ACCOUNTANT ). Таким образом, мы настраиваем метод следующим образом:

 public function getRoles() { return explode(',', $this->roles); } 

Пришло время перейти к логике, но перед этим давайте создадим самого первого пользователя root с максимальными правами доступа.

Для пользователя легко заполнить все поля в user таблице, кроме одного: поля password . Мы используем bcrypt для хэширования нашего пароля, поэтому мы не можем вставить в него простой текстовый пароль. Чтобы помочь нам справиться с этой утомительной задачей, мы используем этот онлайн-инструмент . Мой пример SQL INSERT оператора root (хэш, рассчитанный для пароля «test») приведен ниже:

 INSERT INTO `user` (`username`, `password`, `email`, `roles`, `active`) VALUES ('root', '$2a$12$3INF/uK0.pmZAMFW0In0uOrfpq.yaVIK0xwmWO8Yjxhs4m8CC1Ei2', '[email protected]', 'ROLE_ADMIN', '1'); 

У вас есть приглашение от ADMIN

Когда администратор посещает страницу /invite , он может ввести адрес электронной почты, чтобы пригласить кого-то. Затем приложение создаст письмо и отправит его. Письмо будет содержать ссылку для регистрации получателя и уникальный код приглашения, чтобы проверить, что приглашение поступает из надежного источника. Это все. В серверной части адрес электронной почты получателя, код приглашения и срок действия будут сохранены в таблице базы данных.

Код этого метода приведен ниже:

 public function doinviteAction(Request $req) { $email = $req->get('email'); $userid = $req->get('user'); $hash = $this->setInvite($userid, $email); $this->sendMail($email, $hash); $url = $this->generateUrl('invite'); return $this->redirect($url); } private function setInvite($userid, $email) { $em = $this->getDoctrine()->getManager(); $user_repo = $em->getRepository('AppBundle:User'); $user = $user_repo->find($userid); //The user who initiates the invitation $invite = new Invite(); $invite->setInvited($email); $invite->setWhoinvite($user); $now = new \DateTime(); $int = new \DateInterval('P1D'); $now->add($int); $invite->setExpires($now); //Set invitation expirary $random = rand(10000, 99999); $invite->setHash($random); //A random number is used but stricter method to create a verification code can be used. $em->persist($invite); $em->flush(); return $random; } private function sendMail($email, $hash) { $mailer = $this->get('mailer'); $message = $mailer->createMessage() ->setSubject('Someone invites you to join thisdomain.com') ->setFrom('[email protected]') ->setTo($email) ->setBody( $this->renderView('AppBundle:Default:email.html.twig', ['email' => $email, 'hash' => $hash]), 'text/html' ); $mailer->send($message); } 

Мы получаем пользователя, который отправляет приглашение ( $userid ), и пользователя, которого они намереваются пригласить ( $email ). Затем мы создаем запись приглашения и генерируем случайное число, связанное с этим приглашением, в качестве кода приглашения. Вся информация, необходимая для отправки приглашения (включая код приглашения), будет сохранена и сохранена. Наконец, мы возвращаем код приглашения.

После того, как приглашение сохранено, мы отправляем электронное письмо. Я использую почтовый сервис Symfony по умолчанию для этого. Обратите внимание, что для тела письма я использую шаблон Twig для рендеринга богатого контента.

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

 {% if is_granted('ROLE_ADMIN') %} <li><a href="{{path('invite')}}"><i class="large-icon-share"></i>&nbsp;&nbsp;Invite</a></li> {% endif %} 

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

Регистрация начинается

Когда получатель получает приглашение от администратора, он может щелкнуть ссылку и проверить приглашение.

Действующее приглашение должно удовлетворять следующим условиям:

  1. Письмо действительно приглашено.
  2. Срок действия приглашения не истек.
  3. Код подтверждения проверен и связан с этим электронным письмом.

Все вышеперечисленное делается в соответствующем действии контроллера:

 public function preregisterAction(Request $req) { $email = $req->get('email'); $invitationCode = $req->get('code'); try { $this->verify($email, $invitationCode); } catch (\Exception $e) { $this->addFlash('error', $e->getMessage()); return $this->redirectToRoute('error'); } $registration = new User(); $form = $this->createForm(new RegistrationType(), $registration, ['action' => $this->generateUrl('create'), 'method' => 'POST']); return $this->render('AppBundle:Default:register2.html.twig', ['form' => $form->createView(), 'email' => $email]); } private function verify($email, $code) { $repo = $this->getDoctrine()->getManager()->getRepository('AppBundle:Invite'); $invites = $repo->findBy(['invited' => $email]); if (count($invites) == 0) { throw new \Exception("This email is not invited."); } $invite = $invites[0]; $exp = $invite->getExpires(); $now = new \DateTime(); if ($exp < $now) { throw new \Exception("Invitation expires."); } if ($invite->getCode() !== $code) { throw new \Exception('Wrong invitation code.'); } } 

Метод verify выполняет проверку и выдает исключение, если что-то не так. В методе preregisterAction мы отловим это исключение и добавим флэш-сообщение на странице ошибок, чтобы предупредить пользователя. Однако, если проверка прошла успешно, процесс регистрации перейдет к следующему шагу, отображая регистрационную форму.

Вывод

Это подводит нас к концу первой части Symfony 2 Security Management, которая охватывает настройку приложения (database и security.yml ) и этап предварительной регистрации, на котором пользователь проверяет с помощью приложения, что они действительно приглашены.

В нашей части 2 мы рассмотрим следующие два шага: регистрация и вход в систему. Мы также разработаем наш обработчик после входа в систему для выполнения некоторых действий для конкретного приложения после успешного входа пользователя.

Будьте на связи!