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

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

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

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

Best Practice

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

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

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

Best Practice

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

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

Note

Argon2i - это алгоритм хеширования, рекомендованный стандартами индустрии, но оне будет доступен вам, если вы не используете PHP 7.2+ или если у вас не установлено расширение libsodium. bcrypt достаточно для большинства приложений.

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

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

    providers:
        database_users:
            entity: { class: App\Entity\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.yaml, аннотацию @Security и использование isGranted напрямую в сервисе security.authorization_checker.

Best Practice

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

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

Best Practice

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

Аннотация @Security

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

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

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

/**
 * Displays a form to create a new Post entity.
 *
 * @Route("/new", name="admin_post_new")
 * @Security("has_role('ROLE_ADMIN')")
 */
public function new()
{
    // ...
}

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

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
use App\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("user.getEmail() == post.getAuthorEmail()")
 */
public function edit(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/Entity/Post.php
// ...

class Post
{
    // ...

    /**
     * Is the given User the author of this 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
12
use App\Entity\Post;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
use Symfony\Component\Routing\Annotation\Route;

/**
 * @Route("/{id}/edit", name="admin_post_edit")
 * @Security("post.isAuthor(user)")
 */
public function edit(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
27
28
29
30
31
32
33
34
35
/**
 * @Route("/{id}/edit", name="admin_post_edit")
 */
public function edit($id)
{
    $post = $this->getDoctrine()
        ->getRepository(Post::class)
        ->find($id);

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

    if (!$post->isAuthor($this->getUser())) {
        $this->denyAccessUnlessGranted('edit', $post);
    }
    // эквивалентный код без использования сокращения "denyAccessUnlessGranted()":
    //
    // использовать Symfony\Component\Security\Core\Exception\AccessDeniedException;
    // использовать Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
    //
    // ...
    //
    // public function __construct(AuthorizationCheckerInterface $authorizationChecker) {
    //      $this->authorizationChecker = $authorizationChecker;
    // }
    //
    // ...
    //
    // if (!$this->authorizationChecker->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
namespace App\Security;

use App\Entity\Post;
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;

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

    private $decisionManager;

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

    protected function supports($attribute, $subject)
    {
        if (!in_array($attribute, [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, ['ROLE_ADMIN'])) {
                    return true;
                }

                break;

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

                break;
        }

        return false;
    }
}

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

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

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

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

 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 edit($id)
{
    $post = ...; // query for the post

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

    // использовать Symfony\Component\Security\Core\Exception\AccessDeniedException;
    // использовать Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface
    //
    // ...
    //
    // public function __construct(AuthorizationCheckerInterface $authorizationChecker) {
    //      $this->authorizationChecker = $authorizationChecker;
    // }
    //
    // ...
    //
    // if (!$this->authorizationChecker->isGranted('edit', $post)) {
    //    throw $this->createAccessDeniedException();
    // }
    //
    // ...
}

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

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

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

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

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


Далее: Web Assets

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