Как я могу реализовать список контроля доступа в своем приложении Web MVC?

Первый вопрос

Пожалуйста, не могли бы вы объяснить мне, как самый простой ACL может быть реализован в MVC.

Вот первый подход использования Acl в контроллере …

<?php class MyController extends Controller { public function myMethod() { //It is just abstract code $acl = new Acl(); $acl->setController('MyController'); $acl->setMethod('myMethod'); $acl->getRole(); if (!$acl->allowed()) die("You're not allowed to do it!"); ... } } ?> 

Это очень плохой подход, и минус заключается в том, что мы должны добавить часть кода Acl в каждый метод контроллера, но нам не нужны никакие дополнительные зависимости!

Следующий подход заключается в том, чтобы сделать все методы контроллера private и добавить код ACL в метод __call контроллера.

 <?php class MyController extends Controller { private function myMethod() { ... } public function __call($name, $params) { //It is just abstract code $acl = new Acl(); $acl->setController(__CLASS__); $acl->setMethod($name); $acl->getRole(); if (!$acl->allowed()) die("You're not allowed to do it!"); ... } } ?> 

Это лучше, чем предыдущий код, но основные минусы …

  • Все методы контроллера должны быть приватными
  • Мы должны добавить код ACL в каждый метод __call каждого контроллера.

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

Каково решение? И в чем лучшая практика? Где я могу вызвать функции Acl, чтобы решить разрешить или запретить метод, который должен быть выполнен.

Второй вопрос

Второй вопрос заключается в получении роли с помощью Acl. Представим себе, что у нас есть гости, пользователи и друзья пользователей. Пользователь имеет ограниченный доступ к просмотру своего профиля, который могут просматривать только друзья. Все гости не могут просматривать профиль этого пользователя. Итак, вот логика ..

  • мы должны обеспечить, чтобы вызываемый метод был профилем
  • мы должны определить владельца этого профиля
  • мы должны определить, является ли зритель владельцем этого профиля или нет
  • мы должны прочитать правила ограничения этого профиля
  • мы должны решить выполнить или не выполнить метод профиля

Главный вопрос заключается в обнаружении владельца профиля. Мы можем определить, кто является владельцем профиля, только выполнив метод модели $ model-> getOwner (), но Acl не имеет доступа к модели. Как мы можем это реализовать?

Надеюсь, что мои мысли ясны. Извините за мой английский.

Спасибо.

Первая часть / ответ (реализация ACL)

По моему скромному мнению, лучший способ приблизиться к этому – это использовать узор декоратора. В основном это означает, что вы берете свой объект и помещаете его в другой объект, который будет действовать как защитная оболочка. Это НЕ потребовало бы расширения исходного класса. Вот пример:

 class SecureContainer { protected $target = null; protected $acl = null; public function __construct( $target, $acl ) { $this->target = $target; $this->acl = $acl; } public function __call( $method, $arguments ) { if ( method_exists( $this->target, $method ) && $this->acl->isAllowed( get_class($this->target), $method ) ){ return call_user_func_array( array( $this->target, $method ), $arguments ); } } } 

И это будет так, как вы используете такую ​​структуру:

 // assuming that you have two objects already: $currentUser and $controller $acl = new AccessControlList( $currentUser ); $controller = new SecureContainer( $controller, $acl ); // you can execute all the methods you had in previous controller // only now they will be checked against ACL $controller->actionIndex(); 

Как вы могли заметить, это решение имеет ряд преимуществ:

  1. сдерживание может использоваться на любом объекте, а не только на экземплярах Controller
  2. проверка авторизации происходит вне целевого объекта, а это означает, что:
    • оригинальный объект не отвечает за контроль доступа, придерживается SRP
    • когда вы получаете «разрешение отказано», вы не заблокированы внутри контроллера, больше параметров
  3. вы можете ввести этот защищенный экземпляр в любой другой объект, он сохранит защиту
  4. оберните его и забудьте об этом .. вы можете притвориться, что это оригинальный объект, он будет реагировать на то же самое

Но есть и одна серьезная проблема с этим методом: вы не можете проверить, действительно ли реализуется защищенный объект и интерфейс (который также применяется для поиска существующих методов) или является частью некоторой цепочки наследования.

Вторая часть / ответ (RBAC для объектов)

В этом случае основное различие, которое вы должны распознать, заключается в том, что вы сами объекты домена (например: Profile ) содержат сведения о владельце. Это означает, что для проверки, если (и на каком уровне) у пользователя есть доступ к нему, вам потребуется изменить эту строку:

 $this->acl->isAllowed( get_class($this->target), $method ) 

По существу у вас есть два варианта:

  • Предоставьте ACL объекту, о котором идет речь. Но вы должны быть осторожны, чтобы не нарушать Закон Деметры :

     $this->acl->isAllowed( get_class($this->target), $method ) 
  • Запросите все соответствующие данные и предоставите ACL только то, что ему нужно, что также сделает его более удобным для тестирования модулей:

     $command = array( get_class($this->target), $method ); /* -- snip -- */ $this->acl->isAllowed( $this->target->getPermissions(), $command ) 

Сделайте пару видеороликов, которые помогут вам придумать собственную реализацию:

  • Наследование, полиморфизм и тестирование
  • Не смотрите на вещи!

Боковые заметки

У вас, похоже, довольно общее (и совершенно неверное) понимание того, что такое модель в MVC. Модель не является классом . Если у вас есть класс с именем FooBarModel или что-то, что наследует AbstractModel тогда вы делаете это неправильно.

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

Бизнес-логика домена

( подробнее : здесь и здесь ):

Экземпляры из этой группы классов занимаются вычислением ценностей, проверяют разные условия, реализуют правила продаж и делают все остальное, что вы бы назвали «бизнес-логикой». Они не имеют понятия, как хранятся данные, где они хранятся или даже если хранилище существует на первом месте.

Объект Domain Business не зависит от базы данных. Когда вы создаете счет-фактуру, не имеет значения, откуда поступают данные. Это может быть либо SQL, либо удаленный REST API, либо даже снимок экрана документа MSWord. Бизнес-логика не меняет.

Доступ к данным и их хранение

Экземпляры, сделанные из этой группы классов, иногда называются объектами доступа к данным. Обычно структуры, которые реализуют шаблон Data Mapper (не путайте с ORM одного и того же имени .. никакого отношения). Это будет ваш SQL-запрос (или, возможно, ваш DomDocument, потому что вы храните его в XML).

Кроме двух основных частей, есть еще одна группа экземпляров / классов, которые следует упомянуть:

Услуги

Это – то, где ваши и сторонние компоненты входят в игру. Например, вы можете придумать «аутентификацию» как службу, которая может быть предоставлена ​​вашим собственным или каким-либо внешним кодом. Также «почтовый отправитель» будет сервисом, который может объединить некоторый объект домена с помощью PHPMailer или SwiftMailer или вашего собственного компонента отправителя почты.

Другим источником услуг является абстракция на уровнях доступа к доменам и данным. Они созданы для упрощения кода, используемого контроллерами. Например: для создания новой учетной записи пользователя может потребоваться работа с несколькими объектами и карточками домена . Но, используя сервис, ему потребуется только одна или две строки в контроллере.

Что вы должны помнить при предоставлении услуг, так это то, что весь слой должен быть тонким . В услугах нет бизнес-логики. Они существуют только для жонглирования объектов, компонентов и карт домена.

Одна из вещей, которые они имеют вместе, заключается в том, что службы не влияют на слой «Вид» любым прямым способом и являются автономными до такой степени, что они могут (и часто выходить) использоваться вне структуры MVC. Кроме того, такие самоподдерживающиеся структуры значительно облегчают переход к другой структуре / архитектуре из-за чрезвычайно низкого взаимодействия между сервисом и остальной частью приложения.

ACL и контроллеры

Прежде всего: это разные вещи / слои чаще всего. Когда вы критикуете примерный код контроллера, он складывает обе вместе – наиболее очевидно, слишком туго.

tereško уже изложил способ, как вы могли бы отделить это больше с рисунком декоратора.

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

С одной стороны, вы хотите иметь контроллеры, которые просто выполняют задание, которому они подчиняются (команда или действие, давайте назовем его командой).

С другой стороны, вы хотите включить ACL в свое приложение. Область работы этих ACL должна быть – если бы я правильно понял ваш вопрос – чтобы контролировать доступ к определенным командам ваших приложений.

Поэтому для такого контроля доступа требуется нечто другое, которое объединяет эти два элемента. Основываясь на контексте, в котором выполняется команда, вызывается ACL, и необходимо решить, будет ли конкретная команда выполняться конкретным субъектом (например, пользователем).

Подведем итог тому, что у нас есть:

  • команда
  • ACL
  • пользователь

Компонент ACL является центральным здесь: ему нужно знать хотя бы что-то о команде (чтобы точно определить команду), и она должна быть в состоянии идентифицировать пользователя. Обычно пользователи легко идентифицируются с помощью уникального идентификатора. Но часто в web-приложениях есть пользователи, которые вообще не идентифицированы, часто называются гостями, анонимными, всеми и т. Д. В этом примере мы предполагаем, что ACL может потреблять пользовательский объект и инкапсулировать эти данные. Пользовательский объект привязан к объекту запроса приложения, и ACL может его использовать.

Как насчет идентификации команды? Ваша интерпретация шаблона MVC предполагает, что команда является составной частью имени класса и имени метода. Если мы посмотрим более внимательно, есть даже аргументы (параметры) для команды. Итак, правильно спросить, что именно идентифицирует команду? Имя класса, имя метода, число или имена аргументов, даже данные внутри любого из аргументов или смесь всего этого?

В зависимости от того, какой уровень детализации вам нужен, чтобы идентифицировать команду в ACL'ing, это может сильно различаться. В этом примере давайте просто укажем, что команда идентифицируется именем класса и имени метода.

Таким образом, контекст того, как эти три части (ACL, Command и User) принадлежат друг другу, теперь более ясен.

Можно сказать, что с мнимым компонентом ACL мы уже можем сделать следующее:

 $acl->commandAllowedForUser($command, $user); 

Просто посмотрите, что здесь происходит: заставив команду и пользователя идентифицировать, ACL может сделать это. Работа ACL не связана с работой как объекта пользователя, так и конкретной команды.

Остается только одна часть, это не может жить в воздухе. И это не так. Таким образом, вам нужно найти место, где нужно управлять доступом. Давайте посмотрим, что происходит в стандартном веб-приложении:

 User -> Browser -> Request (HTTP) -> Request (Command) -> Action (Command) -> Response (Command) -> Response(HTTP) -> Browser -> User 

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

 User -> Browser -> Request (HTTP) -> Request (Command) 

В какой-то момент вашего приложения вы знаете, что конкретный пользователь попросил выполнить конкретную команду. Вы уже выполняете какой-то ACL'ing здесь: если пользователь запрашивает команду, которая не существует, вы не позволяете этой команде выполнять. Таким образом, где-либо, что случается в вашем приложении, может быть хорошим местом для добавления «реальных» проверок ACL:

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

Место, где сопоставление конкретного HTTPRequest отображается на команду, часто называют Routing . Поскольку у Routing уже есть задача найти команду, почему бы не расширить ее, чтобы проверить, действительно ли команда разрешена для ACL? Например, расширив Router до Router поддерживающего ACL: RouterACL . Если ваш маршрутизатор еще не знает User , тогда Router не подходит, потому что для ACL'ing необходимо работать не только с командой, но и с пользователем. Таким образом, это место может меняться, но я уверен, что вы можете легко найти место, которое вам нужно продлить, потому что это место заполняет требование пользователя и команды:

 User -> Browser -> Request (HTTP) -> Request (Command) 

Пользователь доступен с самого начала, сначала команду с Request(Command) .

Поэтому вместо того, чтобы проверять ACL внутри конкретной реализации каждой команды, вы размещаете ее перед ней. Вам не нужны какие-либо тяжелые шаблоны, магия или что-то еще, ACL выполняет эту работу, пользователь выполняет свою работу, и особенно команда выполняет свою работу: просто команда, ничего больше. Команда не заинтересована в том, применимы ли к ней роли, если они где-то охраняются или нет.

Так что просто держите вещи друг от друга, которые не принадлежат друг другу. Используйте небольшую переформулировку принципа единой ответственности (SRP) : должна быть только одна причина для изменения команды – потому что команда изменилась. Не потому, что теперь вы вводите ACL'ing в свое приложение. Не потому, что вы переключите объект User. Не потому, что вы переходите от интерфейса HTTP / HTML к интерфейсу SOAP или командной строки.

ACL в вашем случае управляет доступом к команде, а не самой командой.

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

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

edit : требуется ли вам доступ к базе данных, сервер LDAP и т. д., ортогонально этому вопросу. Я хотел сказать, что вы можете реализовать авторизацию на основе URL-адресов вместо методов контроллера. Они более надежны, потому что вы, как правило, не будете изменять свои URL-адреса (тип области URL-адресов открытого интерфейса), но вы можете также изменить реализации своих контроллеров.

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