Загадки авторизации в Yii

15 декабря, 2014
Метки:

В своем первом проекте на Yii я не понимала, как работает магия авторизации. Вызываешь метод Yii::$app->user->login() и пользователь авторизован. А как же пароль? Где он передается, как проверяется? В каком классе вообще искать этот волшебный метод login()?
Сегодня я хочу подробно рассказать о том, как работает авторизация в Yii2. Почему в Yii2, а не в Yii? Потому, что если вы только начинаете изучать Yii, стоит сразу разбираться во второй ветке, а не в первой, которую скоро и вовсе перестанут поддерживать.

Часть 1: классы и их связи

И так, что вообще из себя представляет Yii::$app->user? Yii::$app->user — это компонент приложения. Класс, отвечающий за идентификацию пользователя прописан в конфиге

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

Этот класс уже сгенерирован по умолчанию в любом проекте Yii. И именно он вводит в заблуждение. Класс app\models\User является обычным объектом приложения, реализующим интерфейс \yii\web\IdentityInterface, те в обязательном порядке должен реализовать лишь следующие несколько методов:

     public static function findIdentity($id)
     public static function findIdentityByAccessToken($token, $type = null)
     public function getId()
     public function getAuthKey()
     public function validateAuthKey($authKey)

А где же метод login() ? Дело в том, что класс app\models\User это лишь вспомогательный класс, а не сам компонент Yii::$app->user. Сам компонент создается в момент создания приложения, как часть его ядра. В методе preInit базового класса Application создаются все компоненты приложения. Для любого web приложения доступны следующие компоненты

   'log' => ['class' => 'yii\log\Dispatcher'],
   'view' => ['class' => 'yii\web\View'],
   'formatter' => ['class' => 'yii\i18n\Formatter'],
   'i18n' => ['class' => 'yii\i18n\I18N'],
   'mailer' => ['class' => 'yii\swiftmailer\Mailer'],
   'urlManager' => ['class' => 'yii\web\UrlManager'],
   'assetManager' => ['class' => 'yii\web\AssetManager'],
   'security' => ['class' => 'yii\base\Security'],
   'request' => ['class' => 'yii\web\Request'],
   'response' => ['class' => 'yii\web\Response'],
   'session' => ['class' => 'yii\web\Session'],
   'user' => ['class' => 'yii\web\User'],
   'errorHandler' => ['class' => 'yii\web\ErrorHandler'],

Среди которых и user, который представляет собой экземпляр класса yii\web\User. И в этом классе и реализованы те самые таинственные методы login(), logout(), can() и другие.
Впрочем метод login() лишь сохраняет во внутренней переменной данные класса идентификации и дергает поведения на beforeLogin() и afterLogin(). Те вызывать данный метод нужно уже после того, как проверим пароль и права доступа. Этот метод не авторизует пользователя, а лишь делает его авторизованным.

Часть 2: Процесс авторизации

С классами разобрались, теперь посмотрим на сам процесс авторизации в том виде, в котором он представлен в только что созданном приложении Yii2.

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();
        } else {
            return $this->render('login', [
                'model' => $model,
            ]);
        }
    }

Если пользователь уже авторизован, те в приватной переменной $_identity класса yii\web\User уже есть данные, то просто перенаправляем его с этой страницы на главную.
Если же нет, то создаем экземпляр модели LoginForm и вызываем метод login класса LoginForm. Если этот этап прошел успешно — пользователь авторизован. После того как пользователь авторизован можно производить какие-либо действия над ним. К примеру привязать роль или назначить права, используя Yii::$app->user.

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

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

        return $this->_user;
    }

Если валидация прошла, то сделаем пользователя, найденного по username в методе getUser(), авторизованным. Особого внимания заслуживает валидация параметров класса LoginForm

 public function rules()
    {
        return [
            // username and password are both required
            [['username', 'password'], 'required'],
            // rememberMe must be a boolean value
            ['rememberMe', 'boolean'],
            // password is validated by validatePassword()
            ['password', 'validatePassword'],
        ];
    }

Поле password проверяется методом validatePassword.

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

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

Если пользователь не найден или метод app\models\User->validatePassword() вернул null, валидация не пройдет и вернет ошибку.

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

Если вы храните пароль зашифрованным, это нужно учесть в данном методе, прежде чем сравнивать строки.
Надеюсь после прочтения прошлых двух частей статьи стало понятно, как работает авторизация в Yii2.

Часть 3: сторонняя авторизация

Хотела расписать, как сделать авторизацию на основе OpenID, но нашла готовое решение от отца-основателя Yii. Смотрим на github. Отличное расширение, поддерживающее на данный момент 10 сервисов, таких как Google, Yandex, Facebook и тп. Так же дающее возможность писать свои клиенты на основе базовых классов. Так что изобретать велосипед не буду, а лишь расскажу как связать авторизацию соц сервисов со своим приложением.
Как установить и использовать сервисы подробно расписано в ReadMe проекта yii2-authclient. Но что дальше?
После успешной авторизации вызывается метод, прописанный в настройках action auth

public function actions()
    {
        return [
            'error' => [
                'class' => 'yii\web\ErrorAction',
            ],
            ...
            'auth' => [
                'class' => 'yii\authclient\AuthAction',
                'successCallback' => [$this, 'successCallback'],
            ],
        ];
    }

  public function successCallback($client)
    {
        $attributes = $client->getUserAttributes();
        //АВТОРИЗАЦИЯ УСПЕШНО ПРОЙДЕНА. ПИШЕМ ТУТ СВОЙ КОД
    }

Авторизация на стороннем сервисе пройдена успешно, $client->getUserAttributes() вернет все данные пользователя, полученные от стороннего сервиса. Теперь если этого достаточно, можно идентифицировать пользователя:

 public function successCallback($client)
    {
        $attributes = $client->getUserAttributes();
        $user = User::setFromOpenID($attributes);
        Yii::$app->user->login($user);
    }

Нужно помнить, что класс User должен реализовывать IdentityInterface. В моем примере метод setFromOpenID присваивает параметры, полученные от стороннего сервиса к моему объекту user и метод login делает пользователя авторизованным.
Важно понимать, что метод successCallback вызывается сторонним сервисом асинхронно. Пользователь не видит вызова этого метода. Поэтому на данном этапе не нужно никаких редиректов или рендеринга представлений. После вызова successCallback произойдет редирект successUrl, который по умолчанию по умолчанию установлен в Yii::$app->getUser()->getReturnUrl(). И на этой странице уже можно использовать пользователя через Yii::$app->user.
Если помимо авторизации на стороннем сервисе вам нужно связать авторизованного пользователя со пользователем в своей БД, присвоить ему роли и права, то делать это нужно так же в successCallback. Если пользователь авторизован успешно на стороннем сервисе, но отсутствует в вашей базе, не нужно вызывать Yii::$app->user->login($user) . Вместо этого сохраним сообщение об ошибке и при редиректе на страницу отобразим его пользователю.

Надеюсь данная статьи поможем разобраться новичкам с авторизацией в Yii2. Мне самой такая статья очень бы пригодилась в начале знакомства с Yii 🙂


Метки:

Оставить комментарий

10 комментариев »

    Рус

    А можно листинг метода User::setFromOpenID() ?

      Developer

      Метод User::setFromOpenID() должен возвращать объект User. У меня он пытается найти юзера по логину, а если не нашел — то создает нового:

       
       $login = $client->getId().'_'.$attributes['login'];
       $user = static::findOne(['login' => $login]);
              if($user instanceof User)
                return $user;
       else {
                  $user = new User();
                  $user->login = $login;
                  $user->name  = $name;
                  $user->generateAuthKey();
                  if($user->save())
                  {
                      $userRole = Yii::$app->authManager->getRole('user');
                      Yii::$app->authManager->assign($userRole, $user->getId());
                      return $user;
                  }
              }
      return null;
      

      Дальше авторизовываем юзера:

      if($user)
              {
                  Yii::$app->user->login($user, 3600);
              }
      

    Cepefagel

    Статья очень доходчивая, спасибо Вам огромное, теперь буду читать Вас постоянно.

      Developer

      Спасибо 🙂 Пишите в комментариях статьи на какие темы вам бы хотелось почитать.


    polumerk

    Не работает ссылка вот тут «Хотела расписать, как сделать авторизацию на основе OpenID, но нашла готовое решение от отца-основателя Yii. Смотрим на github.»

    Вадег

    Вот в первом комментарии приводите код:
    $login = $client->getId().’_’.$attributes[‘login’];
    $user = static::findOne([‘login’ => $login]);

    Но $attributes[‘login’] не существует же. Заметил что для каждого клиента идентификатор хранится под разными ключами. Как быть то в этом случае. switch писать?

      Developer

      Это нужно для точной идентификации юзера. Например в Facebook есть пользователь с логином Ann и в Яндекс есть такой юзер, но это уже другой человек. По сути у меня логин получился составной — имя-сервиса_логин. В user ищется пользователь и возвращается запись из моей таблицы, где id — первичный уникальный ключ.
      Вот тут есть пример, как с помощью промежуточной таблицы связать разные логины с разных сервисов с одним юзером.


    Вадег

    Был не прав. Все таки уникальный id возвращается под ключом id.


    Саша

    Спасибо! Как раз сейчас с аутентификацией, авторизацией, рбак разбираюсь!


Оставить комментарий:









Копирование материалов разрешено при наличии активной ссылки на источник