Безопасность — это двухэтапный процесс, целью которого является запретить или разрешить доступ к ресурсу для определённых групп пользователей. Первый этап — аутентификация — система идентифицирует пользователя, исходя из предоставленных им данных. На втором этапе система переходит к авторизации и определяет, имеет ли пользователь доступ к определенным данным.
Настройки системы безопасности хранятся в файле 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 %}
<!-- ... -->