Аутентификация через Middleware

Практически на каждом сайте есть раздел для аутентифицированных пользователей. И даже более того, все их действия ограничены авторизацией и ролями. Реализация аутентификации и авторизации на PHP — задача не тривиальная и требует знания множества тонкостей. Если ваше приложение подвязано на MVC, то вам нужно внедрить в процесс диспетчеризации специальную прослойку, где определить: позволить пользователю выполнить какое-то действие или нет.

В случае работы с посредниками (middleware) решение гораздо проще и понятнее. В этой статье мы реализуем аутентификацию в соответствии с PSR-7, используя Expressive и zend-authentication. Создадим простую систему аутентификации на базе логина и пароля.

Стартуем

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

Во-первых, загрузим все зависимости:

$ composer require --dev zendframework/zend-expressive-tooling

Затем создадим модуль Auth:

$ ./vendor/bin/expressive module:create Auth

Теперь мы готовы к самому интересному.

Аутентификация

Компонент zend-authentication работает в зависимости от выбранного адаптера.

Каждый адаптер должен имплементировать интерфейс Zend\Authentication\Adapter\AdapterInterface, где объявлен один единственный метод authenticate():

namespace Zend\Authentication\Adapter;

interface AdapterInterface
{
    /**
     * Аутентификация
     *
     * @return \Zend\Authentication\Result
     * @throws Exception\ExceptionInterface if authentication cannot be performed
     */
    public function authenticate();
}

В адаптере должен быть реализован метод authenticate() с логикой аутентификации, а результат помещается в объект класса Zend\Authentication\Result. В объекте Result хранится результат аутентификации, а также идентификатор пользователя. Базовый класс содержит следующие константы:

namespace Zend\Authentication;

class Result
{
    const SUCCESS = 1;
    const FAILURE = 0;
    const FAILURE_IDENTITY_NOT_FOUND = -1;
    const FAILURE_IDENTITY_AMBIGUOUS = -2;
    const FAILURE_CREDENTIAL_INVALID = -3;
    const FAILURE_UNCATEGORIZED = -4;
}

Поскольку мы хотим аутентифицировать пользователей по username и password, то нам нужно создать для этого адаптер:

// В src/Auth/src/MyAuthAdapter.php:

namespace Auth;

use Zend\Authentication\Adapter\AdapterInterface;
use Zend\Authentication\Result;

class MyAuthAdapter implements AdapterInterface
{
    private $password;
    private $username;

    public function __construct(/* any dependencies */)
    {
        // Likely assign dependencies to properties
    }

    public function setPassword(string $password) : void
    {
        $this->password = $password;
    }

    public function setUsername(string $username) : void
    {
        $this->username = $username;
    }

    /**
     * Performs an authentication attempt
     *
     * @return Result
     */
    public function authenticate()
    {
        // Достаём пользовательские данные из бд
        // результат в $row (ассоциативный массив).
        // Для создания пароля следует воспользоваться
        // PHP функцией password_hash()

        if (password_verify($this->password, $row['password'])) {
            return new Result(Result::SUCCESS, $row);
        }

        return new Result(Result::FAILURE_CREDENTIAL_INVALID, $this->username);
    }
}

Для этого сервиса нам понадобится фабрика:

// В src/Auth/src/MyAuthAdapterFactory.php:

namespace Auth;

use Interop\Container\ContainerInterface;
use Zend\Authentication\AuthenticationService;

class MyAuthAdapterFactory
{
    public function __invoke(ContainerInterface $container)
    {
        // Достаём все зависимости из контейнера и передаём в адаптер
        return new MyAuthAdapter(/* any dependencies */);
    }
}

В данной фабрике мы создаём и возвращаем объект класса MyAuthAdapter. В конструктор можем передать такие зависимости как подключение к БД, которое перед этим вытащим из контейнера.

Сервис аутентификации

Теперь мы создадим сервис Zend\Authentication\AuthenticationService, который возьмёт наш адаптер и проверить пользователя. Сначала создадим фабрику для AuthenticationService:

// В src/Auth/src/AuthenticationServiceFactory.php:

namespace Auth;

use Interop\Container\ContainerInterface;
use Zend\Authentication\AuthenticationService;

class AuthenticationServiceFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new AuthenticationService(
            null,
            $container->get(MyAuthAdapter::class)
        );
    }
}

Данный класс достаёт из контейнера объект MyAuthAdapter и передаёт в AuthenticationService. Класс AuthenticationService принимает два параметра:

  • Хранилище пользовательских данных. Если не задан, то будет использоваться сессия.
  • Адаптер для аутентификации.

Теперь нам необходимо зарегистрировать все зависимости:

// В src/Auth/src/ConfigProvider.php:

use Zend\Authentication\AuthenticationService;

// Обновите метод:
public function getDependencies()
{
    return [
        'factories' => [
            AuthenticationService::class => AuthenticationServiceFactory::class,
            MyAuthAdapter::class => MyAuthAdapterFactory::class,
        ],
    ];
}

Аутентификация через форму

Теперь нам нужно создать форму, в которую пользователь будет вводить свои данные. Данный посредник будет делать следующее:

  • для GET запросов, выведет на страницу форму входа.
  • для POST запросов, примет данные и постарается их обработать.
    • если данные валидны, то перенаправит пользователя на главную страницу
    • в остальных случаях отобразит пользователю форму с сообщениями об ошибках.

Давайте создадим посредника:

// В src/Auth/src/Action/LoginAction.php:

namespace Auth\Action;

use Auth\MyAuthAdapter;
use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Authentication\AuthenticationService;
use Zend\Diactoros\Response\HtmlResponse;
use Zend\Diactoros\Response\RedirectResponse;
use Zend\Expressive\Template\TemplateRendererInterface;

class LoginAction implements ServerMiddlewareInterface
{
    private $auth;
    private $authAdapter;
    private $template;

    public function __construct(
        TemplateRendererInterface $template,
        AuthenticationService $auth,
        MyAuthAdapter $authAdapter
    ) {
        $this->template    = $template;
        $this->auth        = $auth;
        $this->authAdapter = $authAdapter;
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        if ($request->getMethod() === 'POST') {
            return $this->authenticate($request);
        }

        return new HtmlResponse($this->template->render('auth::login'));
    }

    public function authenticate(ServerRequestInterface $request)
    {
        $params = $request->getParsedBody();

        if (empty($params['username'])) {
            return new HtmlResponse($this->template->render('auth::login', [
                'error' => 'The username cannot be empty',
            ]));
        }

        if (empty($params['password'])) {
            return new HtmlResponse($this->template->render('auth::login', [
                'username' => $params['username'],
                'error'    => 'The password cannot be empty',
            ]));
        }

        $this->authAdapter->setUsername($params['username']);
        $this->authAdapter->setPassword($params['password']);

        $result = $this->auth->authenticate();
        if (!$result->isValid()) {
            return new HtmlResponse($this->template->render('auth::login', [
                'username' => $params['username'],
                'error'    => 'The credentials provided are not valid',
            ]));
        }

        return new RedirectResponse('/admin');
    }
}

В данном посреднике происходит два основных действия: отображение формы и аутентификация пользователя при POST запросе.

Вам также понадобится:

  • Создать шаблон login.
  • Добавить путь к шаблону auth в файле конфигурации.

Данные аспекты мы затронем позже.

Далее создадим фабричный класс для нашего посредника:

// В src/Auth/src/Action/LoginActionFactory.php:

namespace Auth\Action;

use Auth\MyAuthAdapter;
use Interop\Container\ContainerInterface;
use Zend\Authentication\AuthenticationService;
use Zend\Expressive\Template\TemplateRendererInterface;

class LoginActionFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new LoginAction(
            $container->get(TemplateRendererInterface::class),
            $container->get(AuthenticationService::class),
            $container->get(MyAuthAdapter::class)
        );
    }
}

Регистрируем новые классы в ConfigProvider:

// В src/Auth/src/ConfigProvider.php,

public function getDependencies()
{
    return [
        'factories' => [
            Action\LoginAction::class => Action\LoginActionFactory::class,
            AuthenticationService::class => AuthenticationServiceFactory::class,
            MyAuthAdapter::class => MyAuthAdapterFactory::class,
        ],
    ];
}

Теперь можем создать новые маршруты: /login будет вызывать LoginAction и реагировать на методы GET или POST:

// в config/routes.php:
$app->route('/login', Auth\Action\LoginAction::class, ['GET', 'POST'], 'login');

Это же действие можно выполнить в две строчки:

// в config/routes.php:
$app->get('/login', Auth\Action\LoginAction::class, 'login');
$app->post('/login', Auth\Action\LoginAction::class);

Посредник аутентификации

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

// В src/Auth/src/Action/AuthAction.php:

namespace Auth\Action;

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;
use Zend\Authentication\AuthenticationService;
use Zend\Diactoros\Response\RedirectResponse;

class AuthAction implements ServerMiddlewareInterface
{
    private $auth;

    public function __construct(AuthenticationService $auth)
    {
        $this->auth = $auth;
    }

    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        if (! $this->auth->hasIdentity()) {
            return new RedirectResponse('/login');
        }

        $identity = $this->auth->getIdentity();
        return $delegate->process($request->withAttribute(self::class, $identity));
    }
}

Наличие аутентифицированного пользователя мы проверяем методом hasIdentity() класса AuthenticationService.

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

namespace App\Action;

use Interop\Http\ServerMiddleware\DelegateInterface;
use Interop\Http\ServerMiddleware\MiddlewareInterface as ServerMiddlewareInterface;
use Psr\Http\Message\ServerRequestInterface;

class FooAction
{
    public function process(ServerRequestInterface $request, DelegateInterface $delegate)
    {
        $user = $request->getAttribute(AuthAction::class);
    }
}

У посредника AuthAction есть зависимости, которые нужно передать через фабрику:

// В src/Auth/src/Action/AuthActionFactory.php:

namespace Auth\Action;

use Interop\Container\ContainerInterface;
use Zend\Authentication\AuthenticationService;
use Exception;

class AuthActionFactory
{
    public function __invoke(ContainerInterface $container)
    {
        return new AuthAction($container->get(AuthenticationService::class));
    }
}

Регистрация новых зависимостей:

// В src/Auth/src/ConfigProvider.php:


public function getDependencies()
{
    return [
        'factories' => [
            Action\AuthAction::class => Action\AuthActionFactory::class,
            Action\LoginAction::class => Action\LoginActionFactory::class,
            AuthenticationService::class => AuthenticationServiceFactory::class,
            MyAuthAdapter::class => MyAuthAdapterFactory::class,
        ],
    ];
}

Подключение аутентификации для отдельных маршрутов

Теперь можем выстроить цепочки классов посредников, чтобы процесс аутентификации срабатывал только на нужных нам маршрутах:

$app->get('/admin', [
    Auth\Action\AuthAction::class,
    App\Action\DashBoardAction::class
], 'admin');

$app->get('/admin/config', [
    Auth\Action\AuthAction::class,
    App\Action\ConfigAction::class
], 'admin.config');

Итог

Это всего лишь один способ реализации поставленной задачи. Мы хотели показать данный процесс в разрезе работы с цепочками посредников.

Данный урок подготовлен для вас командой сайта ruseller.com
Источник урока: https://framework.zend.com/blog/2017-04-26-authentication-middleware.html
Перевел: Станислав Протасевич
Урок создан: 4 Мая 2017
Просмотров: 786
Правила перепечатки


5 последних уроков рубрики "PHP"

^ Наверх ^