Безопасность

Безопасность

Аутентификация и брандмауэры (т.е. получение сертификатов пользователя)

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

Вне зависимости от ваших нужд, аутентификация конфигурируется в security.yml, в основном под ключом firewalls.

Best Practice

Разве что у вас есть две реально разные системы аутентификации и пользователи (например, форма входа для главного сайта и система токенов только для вашего API), мы рекомендуем иметь только одну запись брандмауэра с включённым ключом anonymous.

Большинство приложений имеют только одну систему аутентификации и один набор пользователей. По этой причине, вам нужна только одна запись брандмауэра. Конечно, существуют исключения, особенно если вы разделили разделы веб и API на вашем сайте. Но цель в том, чтобы оставить всё максимально простым.

Кроме того, вам стоит использовать ключ anonymous в вашем брандмауэре. Если вам нужно затребовать, чтобы пользователь выполнил вход в систему для разных разделов вашего сайта (или, может почти для всех разделов), используйте область access_control.

Best Practice

Используйте кодировщик bcrypt для шифрования паролей ваших пользователей.

Если ваши пользователи имеют пароль, то мы рекомендуем шифровать его используя кодировщик bcrypt, вместо традиционного хеширующего кодировщика SHA-512. Главными премуществами ``bcrypt``являются включеие значения соли для защиты от атаки радужной таблицы, и его адаптивная природа, которая позволяет замедлить его для резистентности к атакам поиска перебором.

Имея это в виду, вот настройка аутентификации из нашего приложения, которое использует форму входа для загрузки пользователей из БД:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# app/config/security.yml
security:
    encoders:
        AppBundle\Entity\User: bcrypt

    providers:
        database_users:
            entity: { class: AppBundle:User, property: username }

    firewalls:
        secured_area:
            pattern: ^/
            anonymous: true
            form_login:
                check_path: login
                login_path: login

            logout:
                path: security_logout
                target: homepage

# ... access_control существует, но не показан тут

Tip

Исходный код нашего проекта содержит комментарии, объясняющие каждую часть.

Авторизация (т.е. отказ в доступе)

Symfony предоставляет вам несколько способов внедрения авторизации, включая конфигурацию access_control в security.yml, аннотацию @Security и использование isGranted напрямую в сервисе security.authorization_checker.

Best Practice

  • Для защиты широких паттернов URL, используйте access_control;
  • Всегда, когда возможно, используйте аннотацию @Security;
  • Проверяйте безопасность напрмую в сервисе security.authorization_checker каждый раз, когда у вас более сложная ситуация.

Также существуют разные способы централизации вашей логики авторизации, например с пользовательским избирателем безопасности или с ACL.

Best Practice

  • Для тонких ограничений, определите пользовательский избиратель безопасности;
  • Для ограничения доступа к любому объекту любого пользователя через интерфейс администратора, используйте Symfony ACL.

Аннотация @Security

Для контролирования доступа на основании от-контроллера-к-контроллеру, используйте аннотацию @Security всегда, когда это возможно. Его легко читать и он постоянно размещается над каждым действием.

В нашем приложении, вам нужна ROLE_ADMIN, чтобы создать новый пост. Использование @Security, выглядит так:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
// ...

/**
 * Отображает форму, чтобы создать новую сущность Post.
 *
 * @Route("/new", name="admin_post_new")
 * @Security("has_role('ROLE_ADMIN')")
 */
public function newAction()
{
    // ...
}

Использование выражений для сложных ограничений безопасности

Если ваша логика безопасности немного сложнее, вы можете использовать выражение внутри @Security. В следующем примере, пользователь может получить доступ к контроллеру только если его email совпадает со значением, возвращённым методом getAuthorEmail() в объекте Post:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("user.getEmail() == post.getAuthorEmail()")
 */
public function editAction(Post $post)
{
    // ...
}

Заметьте, что это требует использования ParamConverter, который автоматически запрашивает объект Post и помещает его в аргумент $post. Это делает возможным использование переменной post в выражении.

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

1
2
3
{% if app.user and app.user.email == post.authorEmail %}
    <a href=""> ... </a>
{% endif %}

Самое простое решение - если ваша логика достаточно проста - добавить новый метод в сущность``Post``, которая проверяет является ли заданный пользователь автором:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/AppBundle/Entity/Post.php
// ...

class Post
{
    // ...

    /**
     * Является ли данный User автором этого Post?
     *
     * @return bool
     */
    public function isAuthor(User $user = null)
    {
        return $user && $user->getEmail() == $this->getAuthorEmail();
    }
}

Теперь вы можете повторно использовать этот метод как шаблоне, так и в выражении безопасности:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
use AppBundle\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("post.isAuthor(user)")
 */
public function editAction(Post $post)
{
    // ...
}
1
2
3
{% if post.isAuthor(app.user) %}
    <a href=""> ... </a>
{% endif %}

Проверка разрешеий без @Security

Вышеописанный пример с @Security работает только потому, что мы используем ParamConverter, который предоставляет выражению доступ к переменной post. Если вы не используете это, или если у вас более продвинутый пример использования, вы всегда можете сделать такую же проверку безопасности в PHP:

 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
/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id, EntityManagerInterface $em)
{
    $post = $em->getRepository('AppBundle:Post')
        ->find($id);

    if (!$post) {
        throw $this->createNotFoundException();
    }

    if (!$post->isAuthor($this->getUser())) {
        $this->denyAccessUnlessGranted('edit', $post);
    }
    // эквивалентный код без использования шортката "denyAccessUnlessGranted()":
    //
    // используйте Symfony\Component\Security\Core\Exception\AccessDeniedException;
    // ...
    //
    // if (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
    //    throw $this->createAccessDeniedException();
    // }

    // ...
}

Избиратели безопасности

Если ваша логика безопасности сложная и не может быть централизована в методе вроде isAuthor(), вам стоит использовать пользовательских избирателей. Они намного легче, чем ACL и предоставят вам гибкость, необходимую вам почти по всех случаях.

Для начала, создайте класс избирателя. Следующий пример показывает избирателя, реализующего ту же логику getAuthorEmail(), что использовалась выше:

 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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
namespace AppBundle\Security;

use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
use Symfony\Component\Security\Core\User\UserInterface;
use AppBundle\Entity\Post;

class PostVoter extends Voter
{
    const CREATE = 'create';
    const EDIT   = 'edit';

    /**
     * @var AccessDecisionManagerInterface
     */
    private $decisionManager;

    public function __construct(AccessDecisionManagerInterface $decisionManager)
    {
        $this->decisionManager = $decisionManager;
    }

    protected function supports($attribute, $subject)
    {
        if (!in_array($attribute, array(self::CREATE, self::EDIT))) {
            return false;
        }

        if (!$subject instanceof Post) {
            return false;
        }

        return true;
    }

    protected function voteOnAttribute($attribute, $subject, TokenInterface $token)
    {
        $user = $token->getUser();
        /** @var Post */
        $post = $subject; // $subject must be a Post instance, thanks to the supports method

        if (!$user instanceof UserInterface) {
            return false;
        }

        switch ($attribute) {
            case self::CREATE:
                // если пользователь ялвяется админом, позвольте ему создавать новые посты
                if ($this->decisionManager->decide($token, array('ROLE_ADMIN'))) {
                    return true;
                }

                break;
            case self::EDIT:
                // если пользователь является автором поста, позвольте ему редактировать посты
                if ($user->getEmail() === $post->getAuthorEmail()) {
                    return true;
                }

                break;
        }

        return false;
    }
}

Если вы используете конфигурацию services.yml по умолчанию, то ваше приложение автоматически сконфигурирует вашего избирателя безопасности и внедрит экземпляр AccessDecisionManagerInterface в него благодаря автомонтированию.

Теперь вы можете использовать избирателя с аннотацией @Security:

1
2
3
4
5
6
7
8
/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("is_granted('edit', post)")
 */
public function editAction(Post $post)
{
    // ...
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function editAction($id)
{
    $post = ...; // query for the post

    $this->denyAccessUnlessGranted('edit', $post);

    // или без шортката:
    //
    // используйте Symfony\Component\Security\Core\Exception\AccessDeniedException;
    // ...
    //
    // если (!$this->get('security.authorization_checker')->isGranted('edit', $post)) {
    //    вызвать $this->createAccessDeniedException();
    // }
}

Узнайте больше

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

Включите функцию "Запомнить меня", чтобы позволить вашим пользователям оставаться в системе на долгий период времени.

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

Если ваша компания использует метод логина пользователей, не поддерживаемый Symfony, вы можете разработать вашего собстенного поставщика пользователей и вашего собственного поставщика аутентификации.

Эта документация является переводом официальной документации Symfony и предоставляется по свободной лицензии CC BY-SA 3.0.