A free and open-source book on ZF3 for beginners


16.7. Реализация аутентификации пользователя

Аутентификация - это процесс, при котором пользователь предъявляет свои логин и пароль, а вы решаете, корректны ли его учетные данные. Аутентификация обычно подразумевает, что вы проверяете свою базу данных на наличие заданного логина, и, если такой логин существует, проверяете, совпадает ли хэш, вычисленный по заданному паролю с хэшем пароля, хранимым в базе данных.

Как правило, пароль как он есть не хранится в базе данных. Вместо этого хранится его хэш. Это делается из соображений безопасности.

После того, как алгоритм аутентификации определяет, что логин и пароль верны, он возвращает личность пользователя - его уникальный ID. Личность, как правило, хранится в сессии, так что посетителю сайта не нужно проходить аутентификацию для каждого HTTP-запроса.

В ZF3 для реализации аутентификации пользователя существует специальный компонент - Zend\Authentication. Его можно установить с помощью Composer, набрав следующую команду:

php composer.phar require zendframework/zend-authentication

Для работы механизмов аутентификации, вам также потребуются установленный компонент Zend\Session и настроенный менеджер сессий. За информацией о том, как это сделать, обратитесь к главе Работа с сессиями.

16.7.1. AuthenticationService

Компонент Zend\Authentication предоставляет специальный класс сервиса AuthenticationService, "живущий" в пространстве имен Zend\Authentication. Наиболее полезные методы этого сервиса показаны в таблице 16.1 ниже.

Таблица 16.1. Методы класса AuthenticationService
Метод Описание
authenticate() Выполняет аутентификация пользователя, используя адаптер.
getAdapter() Получает адаптер аутентификации.
setAdapter() Задает адаптер, реализующий алгоритм аутентификации.
getStorage() Возвращает обработчик хранилищ.
setStorage() Задает обработчик хранилищ.
hasIdentity() Возвращает true, если личность пользователя уже хранится в сессии.
getIdentity() Извлекает из сессии личность пользователя.
clearIdentity() Удаляет личность пользователя из сессии.

Как видите из данной таблицы, для выполнения аутентификации можно использовать метод authenticate(). Помимо этого, можно использовать методы hasIdentity(), getIdentity() и clearIdentity() для, соответственно, проверки существования, извлечения и удаления личности пользователя.

Однако, сервис AuthenticationService очень 'универсален' - он ничего не знает о том, как сверять логин и пароль с БД. Он также не умеет сохранять личность пользователя в сессию. Этот сервис предназначен для реализации любых подходящих алгоритма аутентификации и хранилища.

Компонент Zend\Authentication предоставляет несколько адаптеров аутентификации, реализующих некоторые стандартные алгоритмы аутентификации (см. рисунок 16.9) и несколько обработчиков хранилищ, позволяющих сохранять и извлекать личность пользователя (см. рисунок 16.10).

Рисунок 16.9. Стандартные адаптеры аутентификации Рисунок 16.9. Стандартные адаптеры аутентификации

Рисунок 16.10. Стандартные обработчики хранилищ Рисунок 16.10. Стандартные обработчики хранилищ

Для наших целей мы может использовать обработчик хранилищ Session, не внося никаких изменений в его код. Однако, стандартные адаптеры аутентификации нам не подходят, так как мы используем ORM Doctrine. Нам придется написать свой соббственный адаптер аутентификации. К счастью, это довольно просто.

16.7.2. Написание адаптера аутентификации

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

Метод authenticate() возвращает экземпляр класса Zend\Authentication\Result. Класс Result содержит статус аутентификации, сообщение об ошибке и личность пользователя.

Адаптер может иметь и другие методы: например, setEmail() и setPassword(), которые мы будем использовать для передачи адаптеру эл. адреса и пароля пользователя.

Чтобы создать адаптер аутентификации, добавьте файл AuthAdapter.php в каталог Service исходного каталога модуля.

В приложении User Demo мы создаем отдельный модуль User и добавляем в него всю функциональность, относящуюся к аутентификации и управлению пользователями.

Поместите в этот файл следующий код:

<?php
namespace User\Service;

use Zend\Authentication\Adapter\AdapterInterface;
use Zend\Authentication\Result;
use Zend\Crypt\Password\Bcrypt;
use User\Entity\User;

/**
 * Это адаптер, используемый для аутентификации пользователя. Он принимает логин (адрес эл. почты)
 * и пароль, и затем проверяет, есть ли в базе данных пользователь с такими учетными данными.
 * Если такой пользователь существует, сервис возвращает его личность (эл. адрес). Личность
 * сохраняется в сессии и может быть извлечена позже с помощью помощника представления Identity, 
 * предоставляемого ZF3.
 */
class AuthAdapter implements AdapterInterface
{
    /**
     * E-mail пользователя.
     * @var string 
     */
    private $email;
    
    /**
     * Пароль.
     * @var string 
     */
    private $password;
    
    /**
     * Менеджер сущностей.
     * @var Doctrine\ORM\EntityManager 
     */
    private $entityManager;
        
    /**
     * Конструктор.
     */
    public function __construct($entityManager)
    {
        $this->entityManager = $entityManager;
    }
    
    /**
     * Задает эл. адрес пользователя.     
     */
    public function setEmail($email) 
    {
        $this->email = $email;        
    }
    
    /**
     * Устанавливает пароль.     
     */
    public function setPassword($password) 
    {
        $this->password = (string)$password;        
    }
    
    /**
     * Выполняет попытку аутентификации.
     */
    public function authenticate()
    {                
        // Проверяем, есть ли в базе данных пользователь с таким адресом.
        $user = $this->entityManager->getRepository(User::class)
                ->findOneByEmail($this->email);
        
        // Если такого пользователя нет, возвращаем статус 'Identity Not Found'.
        if ($user == null) {
            return new Result(
                Result::FAILURE_IDENTITY_NOT_FOUND, 
                null, 
                ['Invalid credentials.']);        
        }   
        
        // Если пользователь с таким адресом существует, необходимо проверить, активен ли он.
        // Неактивные пользователи не могут входить в систему.
        if ($user->getStatus()==User::STATUS_RETIRED) {
            return new Result(
                Result::FAILURE, 
                null, 
                ['User is retired.']);        
        }
        
        // Теперь необходимо вычислить хэш на основе введенного пользователем пароля и сравнить его
        // с хэшем пароля из базы данных.
        $bcrypt = new Bcrypt();
        $passwordHash = $user->getPassword();
        
        if ($bcrypt->verify($this->password, $passwordHash)) {
            // Отлично! Хэши паролей совпадают. Возвращаем личность пользователя (эл. адрес) для
            // хранения в сессии с целью последующего использования.
            return new Result(
                    Result::SUCCESS, 
                    $this->email, 
                    ['Authenticated successfully.']);        
        }             
        
        // Если пароль не прошел проверку, возвращаем статус ошибки 'Invalid Credential'.
        return new Result(
                Result::FAILURE_CREDENTIAL_INVALID, 
                null, 
                ['Invalid credentials.']);        
    }
}

16.7.3. Создание фабрики для AuthenticationService

После реализации адаптера мы наконец можем создать AuthenticationService. Перед тем, как вы сможете использовать этот сервис, его нужно зарегистрировать в менеджере сервисов. Сперва мы создадим для него фабрику. Добавьте файл AuthenticationServiceFactory.php в каталог Service/Factory и поместите в него следующий код:

<?php
namespace User\Service\Factory;

use Interop\Container\ContainerInterface;
use Zend\Authentication\AuthenticationService;
use Zend\ServiceManager\Factory\FactoryInterface;
use Zend\Session\SessionManager;
use Zend\Authentication\Storage\Session as SessionStorage;
use User\Service\AuthAdapter;

/**
 * Это фабрика, отвечающая за создание сервиса аутентификации.
 */
class AuthenticationServiceFactory implements FactoryInterface
{
    /**
     * Этот метод создает сервис Zend\Authentication\AuthenticationService 
     * и возвращает его экземпляр. 
     */
    public function __invoke(ContainerInterface $container, 
                    $requestedName, array $options = null)
    {
        $sessionManager = $container->get(SessionManager::class);
        $authStorage = new SessionStorage('Zend_Auth', 'session', $sessionManager);
        $authAdapter = $container->get(AuthAdapter::class);

        // Создаем сервис и внедряем зависимости в его конструктор.
        return new AuthenticationService($authStorage, $authAdapter);
    }
}

В описании фабрики мы первым делом создаем экземпляр менеджера сессий (у вас уже должен быть созданный менеджер сессий) и экземпляр обработчика хранилищ Session. После этого мы создаем экземпляр AuthAdapter. Затем мы инстанцируем AuthenticationService и внедряем в него зависимости (обработчик хранилищ и адаптер).

Зарегистрируйте AuthenticationService в файле конфигурации module.config.php как показано ниже:

<?php 
return [
    'service_manager' => [
        'factories' => [
            \Zend\Authentication\AuthenticationService::class 
                => Service\Factory\AuthenticationServiceFactory::class,
            // ...
        ],
    ],
];

16.7.4. Добавление AuthController

Класс AuthController будет иметь два действия:

Рисунок 16.11. Страница входа на сайт Рисунок 16.11. Страница входа на сайт

Рисунок 16.12. Страница входа на сайт - недействительные учетные данные Рисунок 16.12. Страница входа на сайт - недействительные учетные данные

Ниже представлен код класса контроллера AuthController:

<?php

namespace User\Controller;

use Zend\Mvc\Controller\AbstractActionController;
use Zend\View\Model\ViewModel;
use Zend\Authentication\Result;
use Zend\Uri\Uri;
use User\Form\LoginForm;
use User\Entity\User;

/**
 * Этот контейнер отвечает для предоставлению пользователю возможности входа в систему и выхода из нее.
 */
class AuthController extends AbstractActionController
{
    /**
     * Менеджер сущностей.
     * @var Doctrine\ORM\EntityManager 
     */
    private $entityManager;
    
    /**
     * Менеджер аутентификации.
     * @var User\Service\AuthManager 
     */
    private $authManager;
    
    /**
     * Сервис аутентификации.
     * @var \Zend\Authentication\AuthenticationService
     */
    private $authService;
    
    /**
     * Менеджер пользователей.
     * @var User\Service\UserManager
     */
    private $userManager;
    
    /**
     * Конструктор.
     */
    public function __construct($entityManager, $authManager, $authService, $userManager)
    {
        $this->entityManager = $entityManager;
        $this->authManager = $authManager;
        $this->authService = $authService;
        $this->userManager = $userManager;
    }
    
    /**
     * Аутентифицирует пользователя по заданным эл. адресу и паролю.     
     */
    public function loginAction()
    {
        // Извлекает URL перенаправления (если таковой передается). Мы перенаправим пользователя
        // на данный URL после успешной аутентификации.
        $redirectUrl = (string)$this->params()->fromQuery('redirectUrl', '');
        if (strlen($redirectUrl)>2048) {
            throw new \Exception("Too long redirectUrl argument passed");
        }
        
        // Проверяем, есть ли вообще в базе данных пользователи. Если их нет,
        // создаем пользователя 'Admin'.
        $this->userManager->createAdminUserIfNotExists();
        
        // Создаем форму входа на сайт.
        $form = new LoginForm(); 
        $form->get('redirect_url')->setValue($redirectUrl);
        
        // Храним статус входа на сайт.
        $isLoginError = false;
        
        // Проверяем, заполнил ли пользователь форму
        if ($this->getRequest()->isPost()) {
            
            // Заполняем форму POST-данными
            $data = $this->params()->fromPost();            
            
            $form->setData($data);
            
            // Валидируем форму
            if($form->isValid()) {
                
                // Получаем отфильтрованные и валидированные данные
                $data = $form->getData();
                
                // Выполняем попытку входа в систему.
                $result = $this->authManager->login($data['email'], 
                        $data['password'], $data['remember_me']);
                
                // Проверяем результат.
                if ($result->getCode() == Result::SUCCESS) {
                    
                    // Получаем URL перенаправления.
                    $redirectUrl = $this->params()->fromPost('redirect_url', '');
                    
                    if (!empty($redirectUrl)) {
                        // Проверка ниже нужна для предотвращения возможных атак перенаправления
                        // (когда кто-то пытается перенаправить пользователя на другой домен).
                        $uri = new Uri($redirectUrl);
                        if (!$uri->isValid() || $uri->getHost()!=null)
                            throw new \Exception('Incorrect redirect URL: ' . $redirectUrl);
                    }

                    // Если задан URL перенаправления, перенаправляем на него пользователя;
                    // иначе перенаправляем пользователя на страницу Home.
                    if(empty($redirectUrl)) {
                        return $this->redirect()->toRoute('home');
                    } else {
                        $this->redirect()->toUrl($redirectUrl);
                    }
                } else {
                    $isLoginError = true;
                }                
            } else {
                $isLoginError = true;
            }           
        } 
        
        return new ViewModel([
            'form' => $form,
            'isLoginError' => $isLoginError,
            'redirectUrl' => $redirectUrl
        ]);
    }
    
    /**
     * Действие "logout" выполняет операцию выхода из аккаунта.
     */
    public function logoutAction() 
    {        
        $this->authManager->logout();
        
        return $this->redirect()->toRoute('login');
    }
}

Метод loginAction() принимает GET-параметр redirectUrl. "URL перенаправления" - это удобный механизм, работающий в паре с фильтром доступа, который мы опишем позже в этой главе. Когда посетитель сайта пытается перейти на веб-страницу, доступ к которой фильтр доступа запрещает для не вошедших на сайт пользователей, он перенаправляется на страницу "Login" путем передачи URL исходной страницы в качестве "URL перенаправления". После того, как пользователь войдет на сайт, он автоматически будет перенаправлен обратно на исходную страницу. Такой подход значительно улучшает впечатление от сайта.

16.7.5. Добавление шаблона представления для страницы Login

Шаблон представления (файл .phtml) для нашей страницы Login выглядит так:

<?php
$this->headTitle('Sign in');

$this->mainMenu()->setActiveItemId('login');

$form->get('email')->setAttributes([
    'class'=>'form-control', 
    'placeholder'=>'Email address',
    'required' => true,
    'autofocus' => true
    ])
    ->setLabelAttributes([
        'class' => 'sr-only'
    ]);

$form->get('password')->setAttributes([
    'class'=>'form-control', 
    'placeholder'=>'Password',
    'required' => true,
    ])
    ->setLabelAttributes([
        'class' => 'sr-only'
    ]);
?>

<div class="row">
    <div class="col-md-offset-4 col-md-3">
        <form class="form-signin" method="post">
            <h2 class="form-signin-heading">Please sign in</h2>
            <?php if ($isLoginError): ?>
            <div class="alert alert-warning" role="alert">
                Incorrect login and/or password. 
                <a href="<?= $this->url('reset-password') ?>">Forgot password?</a>
            </div>
            <?php endif; ?>
            <?= $this->formLabel($form->get('email')); ?>
            <?= $this->formElement($form->get('email')); ?>
            <?= $this->formLabel($form->get('password')); ?>
            <?= $this->formElement($form->get('password')); ?>
            <div class="checkbox">
                <label>
                    <?= $this->formElement($form->get('remember_me')); ?> Remember me
                </label>
            </div>
            <?= $this->formElement($form->get('redirect_url')); ?>
            <?= $this->formElement($form->get('csrf')) ?>
            <button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
        </form>
    </div>
</div>

Шаблон представления использует шаблон страницы Sign In, предоставляемый CSS-фреймворком Bootstrap. Исходный шаблон можно найти здесь.

16.7.6. Добавление сервиса AuthManager

AuthController работает в паре с сервисом AuthManager. Основная бизнес-логика аутентификации реализована в сервисе. Давайте детально рассмотрим AuthManager.

Так, у сервиса AuthManager есть следующие отвечающие за аутентификацию методы:

Метод login() (см. ниже) использует предоставляемый ZF3 AuthenticationService и написанный нами ранее AuthAdapter для выполнения аутентификации пользователя. Этот метод также принимает аргумент AuthAdapter, который позволяет продлить время "жизни" cookie-данных сессии на срок до 30 дней.

/**
 * Совершает попытку входа на сайт. Если значение аргумента $rememberMe равно true, сессия
 * будет длиться один месяц (иначе срок действия сессии истечет через один час).
 */
public function login($email, $password, $rememberMe)
{   
    // Проверяем, вошел ли пользователь в систему. Если так, не позволяем
    // ему войти дважды.
    if ($this->authService->getIdentity()!=null) {
        throw new \Exception('Already logged in');
    }
        
    // Аутентифицируем пользователя.
    $authAdapter = $this->authService->getAdapter();
    $authAdapter->setEmail($email);
    $authAdapter->setPassword($password);
    $result = $this->authService->authenticate();

    // Если пользователь хочет, чтобы его "запомнили", мы зададим срок действия
    // сессии, равный одному месяцу. По умолчанию, срок действия сессии истекает 
    // через 1 час (как указано в файле config/global.php).
    if ($result->getCode()==Result::SUCCESS && $rememberMe) {
        // Срок действия cookie сессии закончится через 1 месяц (30 дней).
        $this->sessionManager->rememberMe(60*60*24*30);
    }
    
    return $result;
}

Метод logout() удаляет личность пользователя из сессии, тем самым пользователь становится неавторизованным.

/**
 * Осуществляет выход пользователя из системы.
 */
public function logout()
{
    // Позволяет выйти из учетной записи только авторизованному пользователю.
    if ($this->authService->getIdentity()==null) {
        throw new \Exception('The user is not logged in');
    }
    
    // Удаляем личность из сессии.
    $this->authService->clearIdentity();               
}

Top