Yii2: Logowanie

Chyba nikomu kto sięga po framework w celu stworzenia aplikacji webowej nie trzeba tłumaczyć czym jest uwierzytelnienie zwane potocznie logowaniem i po co się je stosuje w aplikacjach webowych. Prawdę mówiąc jest to pierwsza rzecz za jaką się zabieram kiedy mam już zaprojektowaną przynajmniej zgrubnie aplikację.

W Yii1 sprawa jest prosta, istnieje klasa tożsamości z metodą authentication(), która jest wykorzystywana w modelu obsługującym formularz logowania. Po poprawnej weryfikacji informacje na temat użytkownika przekazywane są do komponentu użytkownika CWebUser, za pomocą którego można później sprawdzać uwierzytelnienie.

Framework Yii2 działa troche inaczej, zawiera on szereg komponentów, które można wywołać przez Yii::$app->. Jednym z takich komponentów jest user będący instancją klasy Yii\Web\użytkownik i on odpowiada za uwierzytelnienie.

Niezbędne elementy

Po pierwsze, trzeba mieć model użytkownika, który implementuje IdentityInterface, a nadto, jeśłi uwierzytelnienie ma być oparte o dane zawarte w bazie, model tem powinien być rozszerzeniem ActiveRecord

class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
{
    public static function tableName()
    {
        return 'user';
    }
}

Po drugie, w pliku konfiguracyjnym należy określić klasę która w rzeczywistości zawiera logikę uwierzytelnienia

'components' => [
        'user' => [
            'identityClass' => 'app\models\User',
        ],
    ],

Po trzecie, musisz mieć kontroler z akcją w której następuje rzeczywiste uwierzytelnienie. Tradycyjnie jest to kontoler SiteController z metodą actionLogin():

namespace app\controllers;

class SiteController extends Controller
{
...
    public function actionLogin()
    {
        \\tu uwierzytelnienie
    }
...
}

Po czwarte, jeśli uwierzytelnianie odbywa się na podstawie standardowego formularza logowania potrzebny jest jeszcze odpowiedni model formularza.

namespace app\models;

use Yii;
use yii\base\Model;

class LoginForm extends Model
{
    public $login;
    public $password;
    public $rememberMe = true;

    private $_user = false;

    public function rules()
    {
        return [
            [['login', 'password'], 'required'],
            ['rememberMe', 'boolean'],
            ['password', 'validatePassword'],
        ];
    }

  ...
}

oraz widok:

use yii\helpers\Html;
use yii\bootstrap\ActiveForm;

$this->title = 'Login';
$this->params['breadcrumbs'][] = $this->title;
?>
<div class="site-login">
    <h1><?= Html::encode($this->title) ?></h1>

    <p>Please fill out the following fields to login:</p>

    <?php $form = ActiveForm::begin([
        'id' => 'login-form',
        'options' => ['class' => 'form-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, 'login')->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(); ?>
</div>

Kolejne kroki

Załóżmy więc, że użytkownik wysyła do aplikacji żądanie ‘loguj’ i oczekuje, że zobaczy formularz kontaktowy. Należy więc w metodzie actionLogin() zdefiniować i wyświetlić formularz:

class SiteController extends Controller
{
...
    public function actionLogin()
    {
        $model = new LoginForm();
        ...
        return $this->render('login', [
            'model' => $model,
        ]);    
    }
...
}

Po wypełnieniu i wysłaniu formularza użytkownik ponownie przekierowany jest do akcji actionLogin() gdzie ten formularz musi zostać przetworzony i własnie w tym momencie następuję sprawdzanie tożsamości użytkownika czyli uwierzytelnienie. Najpierw dane z tablicy $_POST są ładowane do modelu formularza a następnie wywołana jest metoda login().

class SiteController extends Controller
{
...
    public function actionLogin()
    {
        $model = new LoginForm();

        if ($model->load(Yii::$app->request->post()) && $model->login()) {
            return $this->goBack();
        }

        return $this->render('login', [
            'model' => $model,
        ]);    
    }
...
}

Pora więc zdefiniować metodę login() modelu formularza. W pierwszej kolejności metoda ta wywołuje walidację danych, które zostały z formularza załadowane do modelu. W tym celu wykorzystuje reguły walidacji zdefiniowane na początku. Jest to między innymi metoda walidacji hasła validatePassword(), która wykorzystuje metodę walidacji hasła dostepną w modelu user. Po udanej walidacji dane przekazywane sa do komponentu user.

class LoginForm extends Model
{
    ...
    public function login()
    {
        if ($this->validate()) {
            return Yii::$app->user->login($this->getUser(), $this->rememberMe ? 3600*24*30 : 0);
        }
        return false;
    }

    public function validatePassword($attribute, $params)
    {
        if (!$this->hasErrors()) {
            $user = $this->getUser();

            if (!$user || !$user->validatePassword($this->password)) {
                $this->addError($attribute, 'Incorrect username or password.');
            }
        }
    }

    public function getUser()
    {
        if ($this->_user === false) {
            $this->_user = User::findByUsername($this->login);
        }

        return $this->_user;
    }
    ...
}

Metoda validatePassword() poprzez wywołanie metody getUser() powoduje zdefiniowanie obiektu klasy user i jednoczesne wczytanie danych użytkownika z bazy na podstawie podanego w formularzu loginu. Następnie walidacja hasła przeniesiona jest do obiektu użytkownika. Jeśli użytkownik nie zostanie odnaleziony w bazie lub jego hasło nie zostanie poprawnie zweryfikowane pojawi się stosowny komunikat.

Pora więc do modelu użytkownika dopisać metody, które powyżej zostały przywołane.

class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
{
    ...
    public static function findByUsername($login){
	return self::findOne(['login'=>$login]);
    }

    public function validatePassword($password){
	return Yii::$app->getSecurity()->validatePassword($password, $this->password);
    }

    ...
}

W metodzie validatePassword() wykorzystano funkcję do sprawdzania zaszyfrowanego hasła. Oczywiście oznacza to, że podczas rejestracji użytkownika w bazie hasła należy szyfrować metodą:

$hash = Yii::$app->getSecurity()->generatePasswordHash($password);

Ponieważ model użytkownika odpowiedzialny za uwierzytelnienie implementuje interfejs IdentityInterface koniecznie trzeba zdefiniować w nim jeszcze kilka funkcji:

class User extends \yii\db\ActiveRecord implements \yii\web\IdentityInterface
{
    public $authKey;
    ...
    public static function findIdentity($id)
    {
        return static::findOne($id);
    }

    public static function findIdentityByAccessToken($token, $type = null)
    {
        throw new NotSupportedException('"findIdentityByAccessToken" is not implemented.');
    }

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

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

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

EDIT:
Pełną wersję plików można znaleźć w moim repozytorium GitHub.

Leave a Reply

%d bloggers like this: