Повышение безопасности и упрощение разработки в Symfony2 при помощи аннотаций и фильтров Доктрины

  • PHP
  • 3,999
  • 1
  • 2
  • 0
  • 2 года назад

Как вы и сами прекрасно понимаете, пользователь должен видеть только свои заказы, свои сообщения и так далее, и ни в коем случае не чужие. Но, конечно, иногда пропустив условие выборки данных, например, забыв указать WHERE в ParamConverter, мы нарушаем это правило.

Я покажу вам как можно легко избавиться от подобного рода проблем не только на определенных запросах и таблицах, а в пределах всего Symfony приложения. И в этом нам помогут аннотации и фильтры Доктрины.

В итоге вы повысите безопасность приложения, упростите код (не придётся прописывать отдельные запросы) и сможете быстрее расширять функционал.

Предположим, у нас есть связанные сущности User и Order. Пользователь должен иметь доступ только к своим заказам.

<?php 

/** @Entity **/
class User
{
  // ...
}

/** @Entity **/
class Order  
{
  // ...

   /**
    * @ManyToOne(targetEntity="User")
    * @JoinColumn(name="user_id", referencedColumnName="id")
    **/
   private $user;
}

В целом, идея заключается в том, что каждый запрос на выборку заказов должен содержать в своем теле подобное условие WHERE user_id = :user_id.

Шаг 1. Создание собственной аннотации для работы с защищенными сущностями

Здесь мы создадим свою собственную новую аннотацию для сущностей Доктрины.

Смысл её должен заключаться в следующем:

  1. Она должна служить флагом для Доктрины, чтобы та добавляла дополнительное условию к выборке.

  2. Она должна предоставлять имя поля, по которому связываются сущности. Создадим класс аннотации, который позволит передать имя поля связки пользователи и заказа:
<?php

namespace Acme\DemoBundle\Annotation;

use Doctrine\Common\Annotations\Annotation;

/**
* @Annotation
* @Target("CLASS")
*/
final class UserAware  
{
   public $userFieldName;
}

Шаг 2. Применение аннотации в сущности заказа

Поставим отметку о том, что сущность заказа фильтруется по полю пользователя.

<?php

namespace Acme\DemoBundle\Entity;

use Acme\DemoBundle\Annotation\UserAware;

/**
* Order entity
*
* @UserAware(userFieldName="user_id")
*/
class Order { ... } 

Тут-то все и происходит. Просто сделав такую отметку, все запросы, связанные с этой сущностью (даже при присоединении сущности в подзапросах) будут дополнятся условием WHERE.

Шаг 3. Создание класса фильтра Доктрины

Создадим и настроем новый фильтр для Доктрины. Более подробную документацию на тему вы найдете в документации к доктрине.

<?php

namespace Acme\DemoBundle\Filter;

use Doctrine\ORM\Mapping\ClassMetaData;  
use Doctrine\ORM\Query\Filter\SQLFilter;  
use Doctrine\Common\Annotations\Reader;

class UserFilter extends SQLFilter  
{
   protected $reader;

   public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias)
   {
       if (empty($this->reader)) {
           return '';
       }

      // The Doctrine filter is called for any query on any entity
      // Check if the current entity is "user aware" (marked with an annotation)
       $userAware = $this->reader->getClassAnnotation(
           $targetEntity->getReflectionClass(),
           'Acme\\DemoBundle\\Annotation\\UserAware'
       );

       if (!$userAware) {
           return '';
       }

       $fieldName = $userAware->userFieldName;

       try {
          // Don't worry, getParameter automatically quotes parameters
           $userId = $this->getParameter('id');
       } catch (\InvalidArgumentException $e) {
          // No user id has been defined
           return '';
       }

       if (empty($fieldName) || empty($userId)) {
           return '';
       }

       $query = sprintf('%s.%s = %s', $targetTableAlias, $fieldName, $userId);

       return $query;
   }

   public function setAnnotationReader(Reader $reader)
   {
       $this->reader = $reader;
   }
}

Шаг 4. Настроим фильтр Доктрины

Для начала включим фильтр в app/config/cofig.yml:

doctrine:  
   orm:
       filters:
           user_filter:
               class:   Acme\DemoBundle\Filter\UserFilter
               enabled: true

Добавим подписчика для каждого запроса, который будет инициализировать наш фильтр с данными о текущем пользователе (AcmeDemoBundle/Resources/config/services.yml).

Реализация конфигуратора:

<?php

namespace Acme\DemoBundle\Filter;

use Symfony\Component\Security\Core\User\UserInterface;

class Configurator  
{
   protected $em;
   protected $securityContext;
   protected $reader;

   public function __construct($em, $securityContext, $reader)
   {
       $this->em              = $em;
       $this->securityContext = $securityContext;
       $this->reader          = $reader;
   }

   public function onKernelRequest()
   {
       if ($user = $this->getUser()) {
           $filter = $this->em->getFilters()->enable('user_filter');
           $filter->setParameter('id', $user->getId());
           $filter->setAnnotationReader($this->reader);
       }
   }

   private function getUser()
   {
       $token = $this->securityContext->getToken();

       if (!$token) {
           return null;
       }

       $user = $token->getUser();

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

       return $user;
   }
}

Шаг 5. Готово

Теперь каждый запрос к таблице заказов будет содержать в своем теле условие user_id = :user_id, будь то простой SELECT или INNER JOIN.

Нашу новую аннотацию можно применять к любой сущности, просто добавьте @UserAware и ко всем запросам будет добавляться условие выборки.

А вот так теперь будет выглядеть ваш контроллер:

<?php

namespace Acme\DemoBundle\Controller;

use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;

class OrderController  
{
   /**
    * Show order action
    * 
    * @param  Order  $order
    * @Template
    */
   public function showAction(Order $order)
   {
       return array('order' => $order);
   }
}

Все последующие запросы будут содержать условие WHERE (допустим id связанного пользователя равно 12345):

<?php

// SELECT * FROM order WHERE id = 1 AND user_id = 12345
$this->em->getRepository('DemoAcmeBundle:Order')->find(1);

// SELECT *
// FROM order_product op
// INNER JOIN order ON (op.order_id = o.id AND o.user_id = 12345)
// WHERE o.id = 987

$this->em->getRepository('DemoAcmeBundle:OrderProduct')
   ->createQueryBuilder('op')
   ->innerJoin('op.order', 'o')
   ->where('o.id = :order_id')
   ->setParameter('order_id', 987)
;

Комментарии

2
dil, 1 год назад
0

Ох спасибо за эти циклы статей! Читаю который день, пытаюсь разобраться. Вопрос есть: код, написанный для просмотра заказов пользователем уже нельзя использовать для просмотра заказа админом?

devacademy, 1 год назад
0

По задумке да, н овы же можете в фильтр добавить свою логику, так что возможности безграничны