Запис #29 "Авторизація в YII2 через базу даних"

Додано: 2016-11-13 11:49:35 'admin

Нещодавно зіткнувся із необхідністю реалізації авторизації на сайті, побудованому на YII2. Чесно кажучи, останній раз я створював такий функціонал десь 2-3 роки назад і тоді я просто використовував розширення yii-users для реалізації цієї задачі на YII1. На цей раз вирішив більш детальніше взнати про методи авторизації на YII2 засобами самого фреймворка. 

Під час вивчення теми я зустрічав багато статтей на тему ролей користувачів. Ті, хто вже пройшов шлях авторизації та управління ролями користувачів поділились на два табори. Один табір стверджує, що ролі необхідно використовувати у таблиці разом із користувачами. Інший табір переконує у важливості використання RBAC модуля самого фреймворка для управління ролями користувачів. Для себе я вияснив, що на сьогодні немає ідеального метода управління ролями і в двух випадках є свої складності та недороботки. Хоча, логічним буде використання можливостей фремворка без всіляких додаткових кастомизацій. Тому я вирішував задачу ролей саме через RBAC. Але в цій темі спробую розкрити методи самої авторизації не чіпаючи ролі користувачів. Нехай це буде окрема тема, на яку буде витрачений окремий час.

Отже. Є ряд питань, на які необхідно дати відповіді перед тим, як починати вивчення та реалізацію авторизації на YII2:

  1. Які дані необхідні для авторизації?
  2. Який модуль обробляє дані авторизації?
  3. Який шлях проходять дані під час авторизації?
  4. Який механізм обробки данних та як відбувається виведення помилок?
  5. Яким чином відбувається "запам'ятовування" авторизації?
  6. Яка реалізація механізму виходу?

Як видно із переліку питань, які я визначив для себе перед вивченням цієї теми - загальна картина авторизації є вже й не такою простою, як задється на перший погляд. Відомо, що із першими кроками після інсталяції YII2 доступна тестова авторизація. Є форма, є тестові дані користувачів. Є повністю реалізована схема авторизації. Але, нажаль, ця схема працює із масивом, який представлений як статична змінна $users у файлі модуля \app\module\User.php. Якщо коротко, то для авторизації через БД необхідно переназначити модель обробки даних на модель, яка буде підвязана під БД та використовувати аналогчні дані але вже не з масиву а з БД. Але ж стаття націлена на пояснення всього інструменту авторизації. 

ДАНІ ДЛЯ АВТОРИЗАЦІЇ

1. Дані, які необхідні для авторизації беруться із форми авторизації. Ця форма генерується контроллером \app\controllers\SiteController.php, метод actionLogin().

public function actionLogin()
{
    if (!Yii::$app->user->isGuest) {
        return $this->goHome();
    }
    $model = new LoginForm();
    if ($model->load(Yii::$app->request->post()) && $model->login()) {
        return $this->goBack();
    }
    return $this->render('login', [
        'model' => $model,
    ]);
}

Цей метод містить у собі код, який перевіряє, чи користувач дійсно є не авторизованим через значення Yii::$app->user->isGuest. Якщо авторизація відсутня - відбувається створення моделі форми авторизації. Сама модель описана в файлі \app\models\LoginForm.php. Саме її методи приймають та опрацьовують дані користувача, якщо вони присутні. Далі відбувається підтягування файлу відображення форми (\app\views\site\login.php).

<?php $form = ActiveForm::begin([
'id' => 'login-form',
'layout' => 'horizontal',
'fieldConfig' => [
'template' => "{label}\n<div class=\"col-lg-3\">{input}</div>\n<div class=\"col-lg-8\">{error}</div>",
'labelOptions' => ['class' => 'col-lg-1 control-label'],
],
]); ?>
<?= $form->field($model, 'username')->textInput(['autofocus' => true]) ?>
<?= $form->field($model, 'password')->passwordInput() ?>
<?= $form->field($model, 'rememberMe')->checkbox([
'template' => "<div class=\"col-lg-offset-1 col-lg-3\">{input} {label}</div>\n<div class=\"col-lg-8\">{error}</div>",
]) ?>
<div class="form-group">
<div class="col-lg-offset-1 col-lg-11">
<?= Html::submitButton('Login', ['class' => 'btn btn-primary', 'name' => 'login-button']) ?>
</div>
</div>
<?php ActiveForm::end(); ?>

У цьому файлі для відображення форми використовується віджет ActiveForm (\yii\bootstrap\ActiveForm.php). Він формує форму із полями loginform-username, loginform-password, loginform-rememberMe. Поля для введення логіну, паролю та відміткою про бажання автологіну у майбутньому. Також формується кнопка відправлення форми login-form.

МЕХАНІЗМ ОБРОБКИ ТА ПРОХІД ДАНИХ КОРИСТУВАЧІВ

2. Після введення даних та натискання кнопки дані з форми відправляються на сервер методом POST за тою самою адресою, з якої ця форма була викликана. В описаному вище випадку дані надійдуть у метод actionLogin() контроллера SiteController (код метода actionLogin() вище). Звідти вони, вже оброблені функцією Yii::$app->request->post() потрапляють у метод load() моделі \app\models\LoginForm.php (взагалі метод load() описаний в базовому класі \yii\base\Model.php і наслідується вже \app\models\LoginForm.php).  Метод load() повертає бульове значення, яке вказує на обробку вхідних данних від форми. Після вдалої обробки данних стануть доступні атрибути моделі форми ($model->username, $model->password, $model->rememberMe). Тут жеж буде проведена обробка данних методом login(). Цей метод використовує метод validate() базового класу моделі (\yii\base\Model.php). Метод validate() є загальним для перевірки вхідних даних і керується правилами або методами, які починаються із префікса validate. В моделі \app\models\LoginForm.php існують такі правила (метод rules()) та валідатор validatePassword(). Саме цей валідатор через метод getUser та статичний метод findByUsername() моделі \app\models\User.php веде пошук користувача та повертає або об"єкт із даними користувача або записує помилку в масив помилок через метод addError().
Отже. Роблячи висновки, можна стверджувати, що кінцевою моделлю обробки данних користувача є модель \app\models\User.php, яка повинна відповідати інтерфейсу \yii\web\IdentityInterface. Ця ж модель прописана в конфігурації \app\config\web.php:

$config = [
'components' => [
'user' => [
'identityClass' => 'app\models\User',
'enableAutoLogin' => true
]
];
];

3. Шлях, який проходять дані під час авторизації був розкритий у п.2. Але для зручності опишу поетапний прохід по класам:

  • \app\controllers\SiteController.php, метод actionLogin();
  • \app\models\LoginForm.php, метод load(); //завантажує дані форми в модуль
  • \app\models\LoginForm.php, метод login(); // повертає бульове значення перевірки данних
  • \yii\base\Model.phpvalidate(); //визначає правильність введених даних
    • \app\models\LoginForm.php, метод rules(); //перевіряє дані на відповідність правилам
    • \app\models\LoginForm.php, метод validatePassword(); //перевіряє пароль користувача
    • \app\models\LoginForm.php, метод getUser(); //повертає дані користувача
      • \app\models\User.php, метод findByUsername(); //пошук даних користувача
  • Yii::$app->user->login(); //завантаження даних користувача у компонент \yii\web\User.php

Останній клас - це компонент, параметри якого описані в конфгурації \app\config\web.php.

4. Обробка даних користувача зводиться до валідації за допомогою правил та методів валідації. Це відбувається під час виклику метода validate(). Якщо один із валідаторів не проходить - він заносить запис про помилку в масим помилок. Якщо масив помилок містить записи - метод validate() повертає значення false і таким чином не дає можливості занести дані користувача в компонент User. А також відбувається завантаження форми авторизації, де будуть у відповідних полях виведені помилки. 
Якщо валідація пройшла успішно - дані користувача передаються, як параметр в метод login() компонента \yii\web\User.php і цей компонент вже перебирає на себе всі функції по авторизації користувача. 

МЕХАНІЗМ АВТОЛОГІНУ

5. "Запам'ятовування" - це одна із функцій того самого компонента \yii\web\User.php. Все, що необхідно зробити, для того, щоб користувач не вводив декілька разів свої дані - це встановити прапорець у формі авторизації, що переведе параметр $model-rememeberMe в значення true. І таким чином під час передачі даних користувача компоненту \yii\web\User.php після валідаці, другим параметром буде надано число в секундах, на який термін "запам'ятати" користувача. Все решта зробить компонент \yii\web\User.php.

ВИХІД КОРИСТУВАЧА

6. Вихід користувача відбувається за рахунок переходу по посиланню, яке передбачає активацію метода logout() контроллера \yii\controllers\SiteController.php. При активації цього метода відбувається запуск функції виходу в компоненті \yii\web\User.php через строку Yii::$app->user->logout(). Як і раніше всю рутину роботу на себе перебере компонент User.

Висновок.

У Yii2 існує потужний інструмент, який керує авторизацією користувачів. Це компонент \yii\web\User.php. Він обладнаний зовнішніми функціями (можна сказати API) для реалізації своїх можливостей. Все, що залишається розробнику - це організувати логіку обробки даних та передати їх компоненту. На початковому рівні, після встановлення фреймворка цю логіку виконує модель \app\models\User.php. Нажаль розробники створили цей файл із такою ж назвою як і сам компонент і це часом вводить в оману при початковому розборі роботи фрймворка. Отже, для того, щоб реалізувати перевірку даних через базу даних необхідно використати свою логіку, яка буде описана в своїй моделі. На щастя мені вдалось знайти інформацію по організації логіки для БД ось тут. Переклавши все на свій лад я отримав:

  1. Потрібно створити таблицю в БД із полями такими як ключі у масиві $users моделі \app\models\User.php
  2. Через GII створити модель для управління таблицею. Нехай це буде \app\models\TblUser.php
  3. Відкрити свіжостворений файл моделі та додати підвязку до інтерфейса \yii\web\IdentityInterface
  4. Додати методи, які передбачені інтерфейсом.
  5. Додати статичний метод findByUsername().
  6. Додати метод validatePassword().

Після всіх маніпуляцій повинно бути щось схоже на:

class TblUser extends \yii\db\ActiveRecord implements IdentityInterface
{
    public static $password;

    /**
     * @inheritdoc
     */
    public static function tableName()
    {
        return 'tbl_user';
    }

    public static function findIdentity($id)
    {
        return static::findOne($id);
    }

    public static function findIdentityByAccessToken($token, $type = null)
    {
        return static::findOne(['access_token' => $token]);
    }

    public function getId()
    {
        return $this->id;
    }

    public function getAuthKey()
    {
        return $this->auth_key;
    }

    public function validateAuthKey($authKey)
    {
        return $this->getAuthKey() === $authKey;
    }

    public static function findByUsername($username)
    {
        $user = self::find()->where('username = :username', [':username' => $username])->one();

        if(is_object($user) && isset($user->username) && strlen($user->username) > 1)
        {
            self::$password = $user->password;

            return $user;
        }

        return null;
    }

    public function validatePassword($password)
    {
        return $this->cryptPassword($password) === self::$password;
    }

    private function cryptPassword($password)
    {
        return md5($password);
    }
}

Після цього в файлі конфігурації \yii\config\web.php потрібно змінити параметр identityClass на 'app\models\TblUser'.

У файлі \app\models\LoginForm.php переназначити всі використання моделі User на TblUser.

Після редагування цих трьох файлів буде доступна авторизація з вибором даних із БД. Але для повної реалізації необхідно задіяти параметр $authKey. Це дозволить проводити авторизацію через cookie.

Коментарі: