Symfony 2 Joboard : Безопасность

Безопасность — это двухэтапный процесс, целью которого является запретить или разрешить доступ к ресурсу для определённых групп пользователей. Первый этап — аутентификация — система идентифицирует пользователя, исходя из предоставленных им данных. На втором этапе система переходит к авторизации и определяет, имеет ли пользователь доступ к определенным данным.

Настройки системы безопасности хранятся в файле app/config/security.yml. Изменим этот файл:

# app/config/security.yml
security:
  role_hierarchy:
      ROLE_ADMIN:       ROLE_USER
      ROLE_SUPER_ADMIN: [ROLE_USER, ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH]
  firewalls:
      dev:
          pattern:  ^/(_(profiler|wdt)|css|images|js)/
          security: false
      secured_area:
          pattern:    ^/
          anonymous: ~
          form_login:
              login_path:  /login
              check_path:  /login_check
              default_target_path: app_joboard_homepage
  access_control:
      - { path: ^/admin, roles: ROLE_ADMIN }
  providers:
      in_memory:
          memory:
              users:
                  admin: { password: 111111, roles: 'ROLE_ADMIN' }
  encoders:
      Symfony\Component\Security\Core\User\User: plaintext

Таким образом мы разрешаем только пользователям с ролью ROLE_ADMIN доступ к url, начинающимся с /admin. В этом примере пользователь admin создан прямо в файле настроек и не подразумевает кодирования пароля.

Для аутентификации пользователей будет использована обычная форма, которую нам предстоит создать. Сначала зададим два маршрута в routing.yml бандла JoboardBundle: один для показа формы, а второй для обработки этой формы.

# src/App/JoboardBundle/Resources/config/routing.yml
login:
  pattern:   /login
  defaults:  { _controller: AppJoboardBundle:Default:login }
login_check:
  pattern:   /login_check
# ...

Нам не надо писать контроллер для пути /login_check, так как firewall автоматически перехватывает и обрабатывает данные, переданные в форме. Но маршрут все же необходимо создать, чтобы Symfony2 мог создать необходимые URL при обработки шаблона с формой.

Теперь создадим action для отображения формы login:

<?php
# src/App/JoboardBundle/Controller/DefaultController.php
namespace App\JoboardBundle\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\Security\Core\SecurityContext;
class DefaultController extends Controller
{
 // ...
 public function loginAction()
 {
   $request = $this->get('request');
   $session = $this->get('session');
   if ($request->attributes->has(SecurityContext::AUTHENTICATION_ERROR)) {
    $error = $request->attributes->get(SecurityContext::AUTHENTICATION_ERROR);
   } else {
     $error = $session->get(SecurityContext::AUTHENTICATION_ERROR);
     $session->remove(SecurityContext::AUTHENTICATION_ERROR);
   }
   return $this->render('AppJoboardBundle:Default:login.html.twig', array(
     'last_username' => $session->get(SecurityContext::LAST_USERNAME),
     'error'         => $error,
   ));
 }
}

При вводе данных система безопасности автоматически обрабатывает форму. Если данные были введены неверно, то она считывает ошибки и выдаёт их пользователю в ответе на запрос. Все что нам надо это показать форму login и все ошибки связанные с обработкой запроса. Во всём остальном система безопасности всё выполнит за вас.

Наконец, создадим шаблон src/App/JoboardBundle/Resources/views/Default/login.html.twig:

{% if error %}
   <div>{{ error.message }}</div>
{% endif %}
<form action="{{ path('login_check') }}" method="post">
   <label for="username">Логин:</label>
   <input type="text" id="username" name="_username" value="{{ last_username }}" />
   <label for="password">Пароль:</label>
   <input type="password" id="password" name="_password" />
   <button type="submit">войти</button>
</form>

Теперь, при обращении к странице http://joboard.local/app_dev.php/admin/dashboard{:target=»blank»}, вы увидите форму login и сможете ввести имя пользователя и пароль, указанные в файле security.yml. Только после этого вы сможете увидеть администраторскую часть сайта.

User Providers (системы хранения пользователей)

В процессе аутентификации пользователь вводит свои данные, а именно, имя пользователя и пароль. Работа системы заключается в том, чтобы сравнить полученные данные с имеющимися. Так вот откуда берется список пользователей?

В Symfony2 пользователей можно хранить в любом удобном для вас месте — в файле настроек, в таблице БД, использовать веб-службы и т.д. Все, что связано с пользователями и аутентификацией, называется user provider. По умолчанию Symfony2 поддерживает два самых распространенных вида провайдеров: один хранит пользователей просто в файле настроек системы, а другой загружает их из БД.

В примере выше мы использовали первый вариант.

# app/config/security.yml
# ...
providers:
  in_memory:
      memory:
          users:
              admin: { password: adminpass, roles: 'ROLE_ADMIN' }
# ...

Чаще всего разработчики используют второй подход. Для этого добавим новую таблицу в базу данных. Для начала создадим orm.yml для новой таблицы src/App/JoboardBundle/Resources/config/doctrine/User.orm.yml:

App\JoboardBundle\Entity\User:
  type: entity
  table: user
  id:
      id:
          type: integer
          generator: { strategy: AUTO }
  fields:
      username:
          type: string
          length: 255
      password:
          type: string
          length: 255

Выполним команду doctrine:generate:entities для создания класса сущности:

php app/console doctrine:generate:entities AppJoboardBundle

И обновим базу данных:

php app/console doctrine:schema:update --force

Вам необходимо выполнить одно требование, ваш класс должен быть создан на основе интерфейса UserInterface. То есть ваше представление пользователя может быть абсолютно любым, главное выполнять требования интерфейса. Откройте файл модели User.php и отредактируйте его:

<?php
# src/App/JoboardBundle/Entity/User.php
namespace App\JoboardBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\Security\Core\User\UserInterface;
/**
* User
*/
class User implements UserInterface
{
   /**
    * @var integer
    */
   private $id;
   /**
    * @var string
    */
   private $username;
   /**
    * @var string
    */
   private $password;
   /**
    * Get id
    *
    * @return integer
    */
   public function getId()
   {
       return $this->id;
   }
   /**
    * Set username
    *
    * @param string $username
    * @return User
    */
   public function setUsername($username)
   {
       $this->username = $username;
       return $this;
   }
   /**
    * Get username
    *
    * @return string
    */
   public function getUsername()
   {
       return $this->username;
   }
   /**
    * Set password
    *
    * @param string $password
    * @return User
    */
   public function setPassword($password)
   {
       $this->password = $password;
       return $this;
   }
   /**
    * Get password
    *
    * @return string
    */
   public function getPassword()
   {
       return $this->password;
   }
   public function getRoles()
   {
       return array('ROLE_ADMIN');
   }
   public function getSalt()
   {
       return null;
   }
   public function eraseCredentials()
   {
   }
   public function equals(User $user)
   {
       return $user->getUsername() == $this->getUsername();
   }
}

Мы добавили методы getRoles, getSalt, eraseCredentials и equals, чтобы соответствовать интерфейсу.

Теперь настроим user provider, указав ему наш класс:

# app/config/security.yml
# ...
  providers:
      main:
          entity: { class: App\JoboardBundle\Entity\User, property: username }
  encoders:
      App\JoboardBundle\Entity\User: sha512

Мы также изменили encoders, теперь пароли будут храниться в зашифрованном виде в соответствии с алгоритмом sha512. Итак, все настроено, но необходимо еще создать первого пользователя. Для этого напишем собственную команду для Symfony:

<?php
# src/App/JoboardBundle/Command/JoboardUsersCommand.php
namespace App\JoboardBundle\Command;
use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use App\JoboardBundle\Entity\User;
class JoboardUsersCommand extends ContainerAwareCommand
{
   protected function configure()
   {
       $this
           ->setName('app:joboard:users')
           ->setDescription('Добавление пользователей')
           ->addArgument('username', InputArgument::REQUIRED, 'Логин')
           ->addArgument('password', InputArgument::REQUIRED, 'Пароль')
       ;
   }
   protected function execute(InputInterface $input, OutputInterface $output)
   {
       $username = $input->getArgument('username');
       $password = $input->getArgument('password');
       $em = $this->getContainer()->get('doctrine')->getManager();
       $user = new User();
       $user->setUsername($username);
       $factory = $this->getContainer()->get('security.encoder_factory');
       $encoder = $factory->getEncoder($user);
       $encodedPassword = $encoder->encodePassword($password, $user->getSalt());
       $user->setPassword($encodedPassword);
       $em->persist($user);
       $em->flush();
       $output->writeln(sprintf('Добавлен пользователь %s с паролем %s', $username, $password));
   }
}

Для создания пользователя выполните:

php app/console app:joboard:users admin 111111

Таким образом, мы создали пользователи по имени admin с паролем 111111. Теперь вы можете авторизоваться в администраторской части сайта.

Выход из системы

Выход из системы автоматически обрабатывается firewalls. Достаточно активировать параметр logout:

# app/config/security.yml
security:
  firewalls:
      # ...
      secured_area:
          # ...
          logout:
              path:   /logout
              target: /
  # ...

Вам не требуется писать собственный контроллер для этого, firewall все сделает за вас. Создадим маршрут для выхода из системы:

# src/App/JoboardBundle/Resources/config/routing.yml
# ...
logout:
   pattern: /logout
# ...

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

Осталось только создать ссылку для выхода из системы. Для этого переопределим шаблон user_block.html.twig из SonataAdminBundle. Создайте файл user_block.html.twig в каталоге app/Resources/SonataAdminBundle/views/Core:

{% block user_block %}<a href="{{ path('logout') }}">Выход</a>{% endblock%}

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

Сессия пользователя

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

<?php
$session = $this->get('session');
// устанавливаем значение в сессию
$session->set('foo', 'bar');
// получаем значение из сессии
$foo = $session->get('foo');

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

При просмотре страницы с вакансией, будем сохранять историю просмотров в сессии:

<?php
# src/App/JoboardBundle/Controller/JobController.php
// ...
public function showAction($id)
{
   $em = $this->getDoctrine()->getManager();
   $entity = $em->getRepository('AppJoboardBundle:Job')->getActiveJob($id);
   if (!$entity) {
        throw $this->createNotFoundException('Unable to find Job entity.');
   }
   $session = $this->get('session');
   // получить вакансии, которые уже есть в истории
   $jobs = $session->get('job_history', []);
   $job = [
       'id' => $entity->getId(),
       'position' =>$entity->getPosition(),
       'company' => $entity->getCompany(),
       'companyslug' => $entity->getCompanySlug(),
       'locationslug' => $entity->getLocationSlug(),
       'positionslug' => $entity->getPositionSlug()
   ];
   if (!in_array($job, $jobs)) {
       // добавить текущую вакансию в начало массива
       array_unshift($jobs, $job);
       // обновить истории посещений
       $session->set('job_history', array_slice($jobs, 0, 3));
   }
   $deleteForm = $this->createDeleteForm($id);
   return $this->render('AppJoboardBundle:Job:show.html.twig', array(
       'entity'      => $entity,
       'delete_form' => $deleteForm->createView(),
   ));
}

Откройте базовый шаблон app/Resources/views/base.html.twig и добавьте следующий код после блока {% block content %}{% endblock %}:

<!-- ... -->
<div id="job_history">
  Последние просмотренные:
  <ul>
      {% for job in app.session.get('job_history') %}
          <li>
              <a href="{{ path('app_job_show', { 'id': job.id, 'company': job.companyslug, 'location': job.locationslug, 'position': job.positionslug }) }}">{{ job.position }} - {{ job.company }}</a>
          </li>
      {% endfor %}
  </ul>
</div>
<!-- ... -->

Flash сообщения

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

<?php
// ...
public function publishAction($token)
{
  // ...
  $this->get('session')->getFlashBag()->add('notice', 'Ваша вакансия опубликована на 30 дней.');
  // ...
}

Первый аргумент метода getFlashBag()->add() — идентификатор сообщения, а второй — тело сообщения. Можно хранить любые сообщения в сессии, но чаще всего применяются информационные сообщения и ошибки.

Для показа сообщения пользователю, их надо включить в шаблон. Мы сделаем это в шаблоне base.html.twig, добавьте перед блоком {% block content %}{% endblock %} следующий код:

<!-- ... -->
{% for flashMessage in app.session.flashbag.get('notice') %}
  <div>
      {{ flashMessage }}
  </div>
{% endfor %}
<!-- ... -->