Учебник 3: Защита INVO
В этой главе мы продолжим объяснение структуры INVO, поговорим об имплементации аутентификации, авторизации с помощью событий и плагинов, а также о списке управления доступом (ACL), управляемом Phalcon.
Вход в приложение
Функциональность «войти» позволит нам работать с контроллерами бэкэнда. Разделение между контроллерами бэкэнда и фронтенда является логическим. Все контроллеры расположены в одном каталоге (app/controllers/).
Для входа в систему пользователи должны иметь действительное имя пользователя и пароль. Пользователи хранятся в таблице «users» в базе данных «invo».
Перед началом сессии нам необходимо настроить подключение к базе данных в приложении. Сервис под названием «db» настроен в контейнере сервисов с информацией о подключении. Как и с автозагрузчиком, мы снова получаем параметры из файла конфигурации для настройки сервиса:
use Phalcon\Db\Adapter\Pdo\Mysql as DbAdapter;
// ...
// Database connection is created based on parameters defined in the configuration file
$di->set(
"db",
function () use ($config) {
return new DbAdapter(
[
"host" => $config->database->host,
"username" => $config->database->username,
"password" => $config->database->password,
"dbname" => $config->database->name,
]
);
}
);
Здесь мы возвращаем экземпляр адаптера подключения MySQL. При необходимости вы можете выполнить дополнительные действия, такие как добавление логгера, профайлера или изменить адаптер, настроив его по своему усмотрению.
Следующая простая форма (app/views/session/index.volt) запрашивает информацию для входа. Мы удалили часть HTML-кода, чтобы пример был более лаконичным:
{{ form("session/start") }}
<fieldset>
<div>
<label for="email">
Username/Email
</label>
<div>
{{ text_field("email") }}
</div>
</div>
<div>
<label for="password">
Password
</label>
<div>
{{ password_field("password") }}
</div>
</div>
<div>
{{ submit_button("Login") }}
</div>
</fieldset>
{{ endForm() }}
Вместо использования чистого PHP, как в предыдущем учебнике, мы начали использовать Volt. Это встроенный движок шаблонов, вдохновленный Jinja, предоставляющий более простой и удобный синтаксис для создания шаблонов. Вам не составит труда освоить Volt.
Функция SessionController::startAction (app/controllers/SessionController.php) отвечает за проверку введенных данных в форме, включая проверку наличия действительного пользователя в базе данных:
class SessionController extends ControllerBase
{
// ...
private function _registerSession($user)
{
$this->session->set(
"auth",
[
"id" => $user->id,
"name" => $user->name,
]
);
}
/**
* This action authenticate and logs a user into the application
*/
public function startAction()
{
if ($this->request->isPost()) {
// Get the data from the user
$email = $this->request->getPost("email");
$password = $this->request->getPost("password");
// Find the user in the database
$user = Users::findFirst(
[
"(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'",
"bind" => [
"email" => $email,
"password" => sha1($password),
]
]
);
if ($user !== false) {
$this->_registerSession($user);
$this->flash->success(
"Welcome " . $user->name
);
// Forward to the 'invoices' controller if the user is valid
return $this->dispatcher->forward(
[
"controller" => "invoices",
"action" => "index",
]
);
}
$this->flash->error(
"Wrong email/password"
);
}
// Forward to the login form again
return $this->dispatcher->forward(
[
"controller" => "session",
"action" => "index",
]
);
}
}
Для простоты мы использовали «sha1» для хранения хэшей паролей в базе данных, однако в реальных приложениях этот алгоритм не рекомендуется, используйте «bcrypt» вместо него.
Обратите внимание, что в контроллере доступно множество публичных атрибутов, таких как: $this->flash, $this->request или $this->session. Это сервисы, определенные в контейнере сервисов ранее (app/config/services.php). При первом обращении к ним они вводятся в качестве части контроллера.
Эти сервисы являются «совместными», что означает, что мы всегда получаем доступ к одному и тому же экземпляру, независимо от того, откуда мы вызываем их.
Например, здесь мы вызываем сервис «session», а затем сохраняем идентификацию пользователя в переменной «auth»:
$this->session->set(
"auth",
[
"id" => $user->id,
"name" => $user->name,
]
);
Другим важным аспектом этого раздела является то, как происходит проверка пользователя, сначала проверяется, отправлен ли запрос методом POST:
if ($this->request->isPost()) {
Затем мы получаем параметры из формы:
$email = $this->request->getPost("email");
$password = $this->request->getPost("password");
Теперь необходимо проверить, есть ли пользователь с таким же именем пользователя или электронной почтой и паролем:
$user = Users::findFirst(
[
"(email = :email: OR username = :email:) AND password = :password: AND active = 'Y'",
"bind" => [
"email" => $email,
"password" => sha1($password),
]
]
);
Обратите внимание на использование «связанных параметров», плейсхолдеров :email: и :password: размещаются там, где должны быть значения, затем значения связываются с помощью параметра «bind». Это безопасно заменяет значения для этих столбцов, не создавая угрозы SQL-инъекций.
Если пользователь действителен, мы регистрируем его в сессии и перенаправляем его на панель управления:
if ($user !== false) {
$this->_registerSession($user);
$this->flash->success(
"Welcome " . $user->name
);
return $this->dispatcher->forward(
[
"controller" => "invoices",
"action" => "index",
]
);
}
Если пользователя нет, мы перенаправляем пользователя обратно на действие, где отображается форма:
return $this->dispatcher->forward(
[
"controller" => "session",
"action" => "index",
]
);
Защита бэкэнда
Бэкэнд — это закрытая область, доступ к которой имеют только зарегистрированные пользователи. Поэтому необходимо проверять, что к этим контроллерам имеют доступ только зарегистрированные пользователи. Если вы не вошли в приложение и попытаетесь получить доступ, например, к контроллеру продуктов (который является закрытым), вы увидите экран, подобный этому:
Каждый раз, когда кто-то пытается получить доступ к любому контроллеру/действию, приложение проверяет, имеет ли текущая роль (в сессии) доступ к нему, в противном случае отображает сообщение, как указано выше, и перенаправляет поток на главную страницу.
Теперь давайте разберемся, как приложение этого добивается. Первое, что нужно знать, это то, что есть компонент под названием Dispatcher. Он получает информацию о найденном маршруте от компонента Routing. Затем он отвечает за загрузку соответствующего контроллера и выполнение соответствующего метода действия.
Обычно фреймворк создаёт Dispatcher автоматически. В нашем случае мы хотим выполнить проверку перед выполнением необходимого действия, проверяя, имеет ли пользователь к нему доступ или нет. Для этого мы заменили компонент, создав функцию в загрузчике:
use Phalcon\Mvc\Dispatcher;
// ...
/**
* MVC dispatcher
*/
$di->set(
"dispatcher",
function () {
// ...
$dispatcher = new Dispatcher();
return $dispatcher;
}
);
Теперь мы имеем полный контроль над Dispatcher, используемым в приложении. Многие компоненты в фреймворке вызывают события, которые позволяют нам изменять их внутренний поток работы. Поскольку компонент Dependency Injector действует как связующее звено между компонентами, новый компонент под названием EventsManager позволяет нам перехватывать события, производимые компонентом, перенаправляя события слушателям.
Управление событиями
EventsManager позволяет нам присоединять слушателей к определенному типу события. Тип, который нас сейчас интересует, — «dispatch». Следующий код фильтрует все события, производимые Dispatcher:
use Phalcon\Mvc\Dispatcher;
use Phalcon\Events\Manager as EventsManager;
$di->set(
"dispatcher",
function () {
// Create an events manager
$eventsManager = new EventsManager();
// Listen for events produced in the dispatcher using the Security plugin
$eventsManager->attach(
"dispatch:beforeExecuteRoute",
new SecurityPlugin()
);
// Handle exceptions and not-found exceptions using NotFoundPlugin
$eventsManager->attach(
"dispatch:beforeException",
new NotFoundPlugin()
);
$dispatcher = new Dispatcher();
// Assign the events manager to the dispatcher
$dispatcher->setEventsManager($eventsManager);
return $dispatcher;
}
);
При возникновении события «beforeExecuteRoute» будет уведомлен следующий плагин:
/**
* Check if the user is allowed to access certain action using the SecurityPlugin
*/
$eventsManager->attach(
"dispatch:beforeExecuteRoute",
new SecurityPlugin()
);
При возникновении «beforeException» будет уведомлен другой плагин:
/**
* Handle exceptions and not-found exceptions using NotFoundPlugin
*/
$eventsManager->attach(
"dispatch:beforeException",
new NotFoundPlugin()
);
SecurityPlugin — класс, расположенный в (app/plugins/SecurityPlugin.php). Этот класс реализует метод «beforeExecuteRoute». Это то же имя, что и одно из событий, производимых Dispatcher:
use Phalcon\Events\Event;
use Phalcon\Mvc\User\Plugin;
use Phalcon\Mvc\Dispatcher;
class SecurityPlugin extends Plugin
{
// ...
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher)
{
// ...
}
}
События-хуки всегда получают первый параметр, содержащий контекстную информацию о произошедшем событии ($event) и второй — объект, который произвёл это событие ($dispatcher). Плагины необязательно должны расширять класс Phalcon\Mvc\User\Plugin, но, сделав это, они получают более простой доступ к сервисам, доступным в приложении.
Теперь мы проверяем роль в текущей сессии, проверяя, имеет ли пользователь доступ с помощью списка ACL. Если у пользователя нет доступа, мы перенаправляем его на домашнюю страницу, как описано выше:
use Phalcon\Acl;
use Phalcon\Events\Event;
use Phalcon\Mvc\User\Plugin;
use Phalcon\Mvc\Dispatcher;
class SecurityPlugin extends Plugin
{
// ...
public function beforeExecuteRoute(Event $event, Dispatcher $dispatcher)
{
// Check whether the "auth" variable exists in session to define the active role
$auth = $this->session->get("auth");
if (!$auth) {
$role = "Guests";
} else {
$role = "Users";
}
// Take the active controller/action from the dispatcher
$controller = $dispatcher->getControllerName();
$action = $dispatcher->getActionName();
// Obtain the ACL list
$acl = $this->getAcl();
// Check if the Role have access to the controller (resource)
$allowed = $acl->isAllowed($role, $controller, $action);
if (!$allowed) {
// If he doesn't have access forward him to the index controller
$this->flash->error(
"You don't have access to this module"
);
$dispatcher->forward(
[
"controller" => "index",
"action" => "index",
]
);
// Returning "false" we tell to the dispatcher to stop the current operation
return false;
}
}
}
Создание списка ACL
В приведенном выше примере мы получили ACL с помощью метода $this->getAcl(). Этот метод также реализован в плагине. Теперь мы объясним пошагово, как мы создали список управления доступом (ACL):
use Phalcon\Acl;
use Phalcon\Acl\Role;
use Phalcon\Acl\Adapter\Memory as AclList;
// Create the ACL
$acl = new AclList();
// The default action is DENY access
$acl->setDefaultAction(
Acl::DENY
);
// Register two roles, Users is registered users
// and guests are users without a defined identity
$roles = [
"users" => new Role("Users"),
"guests" => new Role("Guests"),
];
foreach ($roles as $role) {
$acl->addRole($role);
}
Теперь мы определяем ресурсы для каждой области соответственно. Имена контроллеров являются ресурсами, а их действия — доступом к ресурсам:
use Phalcon\Acl\Resource;
// ...
// Private area resources (backend)
$privateResources = [
"companies" => ["index", "search", "new", "edit", "save", "create", "delete"],
"products" => ["index", "search", "new", "edit", "save", "create", "delete"],
"producttypes" => ["index", "search", "new", "edit", "save", "create", "delete"],
"invoices" => ["index", "profile"],
];
foreach ($privateResources as $resourceName => $actions) {
$acl->addResource(
new Resource($resourceName),
$actions
);
}
// Public area resources (frontend)
$publicResources = [
"index" => ["index"],
"about" => ["index"],
"register" => ["index"],
"errors" => ["show404", "show500"],
"session" => ["index", "register", "start", "end"],
"contact" => ["index", "send"],
];
foreach ($publicResources as $resourceName => $actions) {
$acl->addResource(
new Resource($resourceName),
$actions
);
}
Теперь ACL знает о существующих контроллерах и связанных с ними действиях. Роль «Пользователи» имеет доступ ко всем ресурсам как фронтенда, так и бэкэнда. Роль «Гости» имеет доступ только к общедоступной области:
// Grant access to public areas to both users and guests
foreach ($roles as $role) {
foreach ($publicResources as $resource => $actions) {
$acl->allow(
$role->getName(),
$resource,
"*"
);
}
}
// Grant access to private area only to role Users
foreach ($privateResources as $resource => $actions) {
foreach ($actions as $action) {
$acl->allow(
"Users",
$resource,
$action
);
}
}
Ура! ACL теперь готов. В следующей главе мы увидим, как реализуется CRUD в Phalcon и как его можно настроить.
© 2011–2017 Phalcon Framework Team
Licensed under the Creative Commons Attribution License 3.0.
https://docs.phalconphp.com/en/latest/reference/tutorial-invo-2.html