Мы обсуждали разработку Symfony 2 в предыдущих статьях SitePoint и создали клон моего личного приложения Symfony (часть 1 , 2 и 3 ). Но Symfony 2 — гигантский фреймворк, и мы можем охватить еще немало тем.
В этой серии статей, состоящей из двух частей, мы поговорим об очень важной области разработки веб-приложений: аутентификации и авторизации . 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> Invite</a></li> {% endif %}
В этом шаблоне мы используем помощник is_granted
чтобы ограничить отображение пункта меню только для пользователей с is_granted
администратора.
Регистрация начинается
Когда получатель получает приглашение от администратора, он может щелкнуть ссылку и проверить приглашение.
Действующее приглашение должно удовлетворять следующим условиям:
- Письмо действительно приглашено.
- Срок действия приглашения не истек.
- Код подтверждения проверен и связан с этим электронным письмом.
Все вышеперечисленное делается в соответствующем действии контроллера:
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 мы рассмотрим следующие два шага: регистрация и вход в систему. Мы также разработаем наш обработчик после входа в систему для выполнения некоторых действий для конкретного приложения после успешного входа пользователя.
Будьте на связи!