Статьи

Контролируйте доступ пользователей к классам и методам с помощью Rauth

Rauth — это пакет контроля доступа SitePoint для предоставления или ограничения доступа к определенным классам или методам, в основном посредством аннотаций.

Значок открытого замка. Плоский дизайн в стиле EPS 10

В этом уроке мы узнаем, как его использовать.

Почему Раут

Традиционные уровни контроля доступа (ACL) контролируют только маршруты — вы устанавливаете все, что начинается с /admin чтобы оно было доступно только администраторам, и так далее. Это хорошо для большинства случаев, но не когда:

  • Вы хотите контролировать доступ в командной строке (нет маршрутов там)
  • вы хотите, чтобы ваш уровень доступа не изменился, даже если вы измените маршруты

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

Аннотации плохие ™

Несколько «спорно», Rauth по умолчанию с помощью аннотаций для управления доступом. Независимо от того, в каком лагере вы находитесь в отношении аннотаций в PHP, вот почему их использование в случае с Раутом далеко не так неправильно, как об этом думают некоторые :

  • так как вы обычно управляете доступом к контроллерам и действиям в типичном приложении MVC, жестко связывайте их с Rauth, как будто это не только безвредно (контроллеры почти всегда должны быть полностью отброшены и переписаны, если вы меняете фреймворки или структуру приложения в основном), он также дает вам мгновенное понимание того, какой класс / метод имеет какие требования ACL

  • если вам не нравятся аннотации, вы можете предоставить Rauth предварительно кэшированный или предварительно проанализированный список разрешений и классов, к которым они применяются, поэтому можно полностью избежать проблемы с аннотациями.

  • больше нет страха, что аннотации замедляют работу, потому что PHP должен отражаться в рассматриваемых классах и извлекать их каждый раз. При постоянном включении OpCache это происходит только один раз, а с собственной поддержкой кэша Rauth его можно сохранить даже в другом месте, а проход чтения аннотации можно вообще избежать.

Простой пример

Для простого примера давайте создадим два класса, файл index.php и установим Rauth:

 composer require sitepoint / rauth 
 <?php // index.php use SitePoint \ Rauth ; require_once 'vendor/autoload.php' ; $r = new Rauth ( ) ; $user = [ 'groups' = > 'admin' , ] ; $fakeRoutes = [ 'admin' = > [ 'One' , 'adminOnly' ] , 'users' = > [ 'One' , 'users' ] , 'banned' = > [ 'One' , 'banned' ] , 'everyone' = > [ 'Two' , 'everyone' ] , ] ; foreach ( $fakeRoutes as $route ) { require_once $route [ 0 ] . '.php' ; $class = $route [ 0 ] ; $method = $route [ 1 ] ; try { $r - > authorize ( $class , $route [ 1 ] , $user ) ; echo "Success: " ; $class = new $class ( ) ; $class - > $method ( ) ; } catch ( Rauth \ Exception \ AuthException $e ) { echo "Authorizing {$class}::{$route[1]} failed!\n" ; echo $e - > getMessage ( ) . "\n" ; } } 
 <?php // One.php /** * Class One * @auth-groups users */ class One { /** * @auth-groups admin */ public function adminOnly ( ) { echo 'Because the "admin" group is detected, the One::adminOnly method is executed.' ; echo "\n" ; } public function users ( ) { echo '"Users" can use One::users - it inherited from the class @auth declaration' ; echo "\n" ; } /** * @auth-groups banned * @auth-mode none */ public function banned ( ) { echo 'No user with the group "banned" can access One::banned.' ; echo "\n" ; } } 
 <?php // Two.php class Two { public function everyone ( ) { echo "Everyone can access Two::everyone!\n" ; } } 

Если мы теперь запустим это в консоли с:

 php index.php 

мы получим:

 vagrant@homestead : ~ / Code / rauthtest$ php index . php Success : Because the "admin" group is detected , the One : : adminOnly method is executed . Authorizing One : : users failed ! Success : No user with the group "banned" can access One : : banned . Success : Everyone can access Two : : everyone ! 

Если мы изменим группу массива $user на banned , посмотрим, что произойдет:

 vagrant@homestead:~/Code/rauthtest$ php index.php Authorizing One::adminOnly failed! Authorizing One::users failed! Authorizing One::banned failed! Success: Everyone can access Two::everyone! 

В двух словах, Rauth использует требования (все начинается с @auth- in docblocks) и сравнивает их с атрибутами (все, что мы определяем в переменной $user ). Вам решать, как вы на самом деле получите эти атрибуты — возможно, вы будете использовать какой-то готовый пакет, такой как Gatekeeper, или вы будете использовать собственную систему аутентификации, не имеет значения. Важно то, что Раут получает атрибуты в формате, который сопоставим с требованиями к классам / методам.

Примечание по режимам: если у пользователя есть групповой атрибут «admin», он может получить доступ ко всем методам с помощью @auth-groups admin . Если mode установлен на @auth-mode OR (это значение по умолчанию и его можно опустить), то пользователь сможет получить доступ к методам с помощью @auth-groups admin, users, banana , потому что им нужен только один из группы определены. Когда режим AND ( @auth-mode AND ), они должны иметь ВСЕ группы. Если режим NONE как в методе banned() , пользователь НЕ ДОЛЖЕН иметь ЛЮБОЙ из групп.

Помните, что auth- может быть чем угодно — не обязательно должна быть groups . Вы можете использовать совершенно произвольные значения, такие как @auth-banana ripe и тогда пользователь с атрибутом 'banana' => 'ripe' сможет получить доступ к классу / методу.

Пример внедрения зависимости

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

 composer require sitepoint / rauth php - di / php - di nikic / fast - route 

Давайте также Controllers папку Controllers и поместим туда два контроллера:

 <?php // Controllers/OneController.php namespace MyApp ; class OneController { public function homeAction ( ) { echo "This is the home screen!" ; } } 
 <?php namespace MyApp ; /** * Class AnotherController * @package MyApp */ class AnotherController { public function indexAction ( ) { echo "This is the second controller, index action, accessible to all" ; } /** * @auth-groups users */ public function onlyLoggedInAction ( ) { echo "This action can only be accessed by logged in users" ; } /** * @auth-groups users * @auth-mode NONE */ public function onlyLoggedOutAction ( ) { echo "This action can only be accessed by visitors who are not logged in" ; } } 

Нам также потребуется настроить автозагрузчик для загрузки этих контроллеров:

 "autoload" : { "psr-4" : { "MyApp\\" : "Controllers" } } 

И восстановите файлы автозагрузки с:

 composer du 

Следующим шагом является настройка маршрутов и контейнера внедрения зависимостей в соответствии с документацией PHP-DI. Наш файл index.php должен быть изменен так:

 <?php // index.php require_once 'vendor/autoload.php' ; use SitePoint \ Rauth ; use FastRoute \ RouteCollector ; use FastRoute \ Dispatcher ; use DI \ ContainerBuilder ; $containerBuilder = new ContainerBuilder ; $containerBuilder - > addDefinitions ( [ ] ) ; $container = $containerBuilder - > build ( ) ; $routeList = [ [ 'GET' , '/one-home' , [ 'MyApp\OneController' , 'homeAction' ] ] , [ 'GET' , '/another-index' , [ 'MyApp\AnotherController' , 'indexAction' ] ] , [ 'GET' , '/another/loggedin' , [ 'MyApp\AnotherController' , 'onlyLoggedInAction' ] ] , [ 'GET' , '/another/loggedout' , [ 'MyApp\AnotherController' , 'onlyLoggedOutAction' ] ] , ] ; /** @var Dispatcher $dispatcher */ $dispatcher = FastRoute\ simpleDispatcher ( function ( RouteCollector $r ) use ( $routeList ) { foreach ( $routeList as $routeDef ) { $r - > addRoute ( $routeDef [ 0 ] , $routeDef [ 1 ] , $routeDef [ 2 ] ) ; } } ) ; $route = $dispatcher - > dispatch ( $_SERVER [ 'REQUEST_METHOD' ] , $_SERVER [ 'REQUEST_URI' ] ) ; switch ( $route [ 0 ] ) { case FastRoute\ Dispatcher : : NOT_FOUND : die ( '404 not found!' ) ; case FastRoute\ Dispatcher : : METHOD_NOT_ALLOWED : die ( '405 method not allowed' ) ; case FastRoute\ Dispatcher : : FOUND : $controller = $route [ 1 ] ; $parameters = $route [ 2 ] ; $container - > call ( $controller , $parameters ) ; break ; } 

Сначала мы делаем новый пустой контейнер. Затем мы определяем некоторые маршруты (обычно это делается во внешнем файле, например routes.php ). Мы добавляем эти маршруты в диспетчер, чтобы он знал, что с ними делать, и, наконец, мы извлекаем элементы запускаемого маршрута и запускаем соответствующий контроллер. Достаточно просто, верно?

Теперь маршруты должны работать:

Маршруты работают

Пора добавить слой Rauth, чтобы фактически ограничить доступ к некоторым из этих маршрутов.

Сначала мы добавим определение User в контейнер:

 $containerBuilder - > addDefinitions ( [ 'User' = > function ( ) { return [ 'groups' = > 'users' , ] ; } , ] ) ; 

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

Затем, в приведенном ниже случае FOUND , мы создадим экземпляр класса Rauth и создадим массив атрибутов для его подачи, когда пришло время выполнить проверку authorize . Обратите внимание, что по умолчанию мы используем пустой массив groups если пользователь не был определен (т.е. он вышел из системы).

 // [...] case FastRoute\ Dispatcher : : FOUND : $controller = $route [ 1 ] ; $parameters = $route [ 2 ] ; $rauth = new Rauth ( ) ; $attributes = $container - > get ( 'User' ) ? : [ 'groups' = > [ ] ] ; 

Поскольку Rauth создает AuthException при сбое вызова authorize , давайте также AuthException все это в блок try/catch .

 try { $rauth - > authorize ( $controller [ 0 ] , $controller [ 1 ] , $attributes ) ; } catch ( Rauth \ Exception \ AuthException $e ) { die ( "Authorization failed" ) ; } 

Если мы сейчас получим доступ к another/loggedout маршруту another/loggedout с ранее another/loggedout пользователем по умолчанию, он должен потерпеть неудачу, потому что у нас есть группа users для этого пользователя:

Маршрут выхода из системы не удался

Пока another/loggedin группа another/loggedin будет успешной:

вошли в маршрутные работы

Конечно, в процессе разработки вам могут потребоваться более подробные сообщения об ошибках. Для этого каждое AuthException имеет type и содержит несколько объектов Reason . Типом будет что-то вроде «none», если режим был установлен на «NONE», на «ban», если сбой произошел из-за бана (есть разница — см. Следующий раздел) и т. Д. Каждая из причин будет иметь свои собственные группы сбоев (не путать с произвольно названным атрибутом «группа») и список всех объектов в этой группе, которые он имеет против ожидаемых. Возможно, это лучше всего объяснить на примере. Мы можем заменить вызов die() выше на:

 echo 'Failed due to: ' . $e - > getType ( ) . '. ' ; /** @var Rauth\Exception\Reason $reason */ foreach ( $e - > getReasons ( ) as $reason ) { echo 'Blocked by "' . $reason - > group . '" when comparing owned "' . implode ( ", " , $reason - > has ) . '" versus "' . implode ( ", " , $reason - > needs ) . '".' ; } die ( ) ; 

Вы бы хотели, чтобы это было в виде флеш-сообщения или в виде обычного сообщения об ошибке в обычном приложении, но в этом уроке это будет отражено, как будто это хорошо. Этот блок приводит к выводу, подобному этому, если мы снова попробуем loggedout :

сбой маршрута выхода из системы, подробный

Другими словами, он говорит: «Я не ожидаю ничего из« групп »:« пользователи », но я получил« пользователей »».

Не стесняйтесь изменять некоторые жестко закодированные группы в пользовательском массиве и изменять аннотации для классов для дальнейшего тестирования некоторых маршрутов. Проверьте режим AND против OR тоже.

Запреты

Последнее, что мы не упомянули до сих пор: запреты. У Раут есть дополнительная предварительная проверка для запрещенных сущностей, которая предшествует всем остальным. Если выполняется запрет, то авторизация не пройдена, независимо от того, сколько других совпадений совпадает. Например:

 @auth - groups users , admin @auth - ban - groups banned 

Вышеуказанное заблокирует доступ к классу или методу, если у пользователя есть banned группа, независимо от того, есть ли у него также users или admin . Как и другие атрибуты, коллекции банов могут быть произвольными:

 @auth - groups users , admin @auth - ban - tags banana 

Пометьте пользователя banana и он не сможет войти, независимо от остальных атрибутов.

Проверки банов выполняются ДО любой другой проверки, поэтому, если вам просто нужно заблокировать доступ к некоторым классам или методам, основанным на некоторых атрибутах, запрет будет наиболее эффективным подходом. Баны имеют свой собственный тип в исключении:

Запрещенный маршрут

Вывод

Мы взглянули на Rauth , пакет SitePoint для контроля доступа к классам и методам. Мы реализовали это в двух простых демонстрациях и показали, насколько это удобно.

Поскольку Rauth является разработкой SitePoint с открытым исходным кодом , мы активно ищем авторов или просто отзывы по некоторым открытым вопросам — мы полны решимости сделать наши пакеты достойными для производственного использования, и нам нужна ваша помощь для этого.

Вы уже дали шанс Раут? Не могли бы вы? Дайте нам знать об этом в комментариях!