Рефакторинг кода авторизации с помощью социальной сети - vorst.ru

Рефакторинг


Использование аккаунта социальной сети для авторизации

Простой алгоритм подключения расширения yiisoft/yii2-authclient с определением необходимых моделей. Инкапсуляция процедуры, приведения необходимых для регистрации атрибутов, к единому виду. Удаление зависимости алгоритма от типа социальной сети.

Рефакторинг

Использование аккаунта социальной сети для авторизации

Авторизация с помощью социальной сети не должна зависеть от типа аккаунта. В предыдущем посте используется код, где в конструкторе классa common/components/SocialContact.php выполняется оператор switch. При добавлении обработчика для новой социальной сети, придется дописывать код, при этом меняя класс.

Перепишем конструктор так, чтобы класс не нужно было менять и он не зависел от типов социальных сетей.


Convertor

После запроса социальной сети программа получает объект $client. Метод getId() объекта возвращает строку "yandex", "vkontakte" и прочее. Можно использовать эту строку в качестве имени класса, который и будет выполнять необходимый код. Классы можно хранить в подкаталоге, например common/components/convertor/Yandex.php.

  public function __construct($client, $config = [])
  {
    $client_id = $client->getId();
    $attributes = $client->getUserAttributes();
    $this->id = (string)$attributes['id'];
    // convert from individual to a single view of attributes
    $class_name = 'common\\components\\convertor\\' . 
      ucfirst($client_id);
    $convertor = new $class_name();
    $convertor->set($this, $attributes);
    // if email not setted then make it
    if (!$this->email)
      $this->email = "{$attributes['id']}@{$client_id}.net";
    parent::__construct($config);
  }

Собственно сам конвертор, например common/components/convertor/Yandex.php должен реализовать метод set.

namespace commmon\components\convertor;
class Yandex implements Convertor {
  public function set($obj, $attributes)
  {
    $obj->name = $attributes['first_name'] . ' ' . $attributes['last_name'];
    $obj->email = isset($attributes['default_email'])
      ? $attributes['default_email']
      : false;
  }
}

SocialLink

При первом входе с помощью аккаунта социальной сети добавляется запись в модель User. Добавленная запись связывается с социальной сетью. Для этого создается запись в модели SocialLink.

Если же пользователь уже вошел с помощью имени и пароля, а потом нажал еще и на кнопку социальной сети, то можно связать существующую запись в модели User, добавив запись в модель SocialLink.

Таким образом действие выполняется два раза, поэтому стоит выделить его в отдельный метод. Полный текст common/components/SocialContact.php теперь следующий.

namespace common\components;
use Yii;
use yii\base\BaseObject;
use yii\base\InvalidValueException;
use common\models\SocialLink;
use common\models\User;
class SocialContact extends BaseObject
{
  public $id;
  public $name;
  public $email;
  private $_link;
  /**
   * Retrieve id, name, email and, may be more.
   * 
   * @param string social client
   * @param array attributes (OAuth2 response)
   */
  public function __construct($client, $config = [])
  {
    $client_id = $client->getId();
    $attributes = $client->getUserAttributes();
    $this->id = (string)$attributes['id'];
    // convert from individual to a single view of attributes
    $class_name = 'common\\components\\convertor\\' .
      ucfirst($client_id);
    $convertor = new $class_name();
    $convertor->set($this, $attributes);
    // if email not setted then make it
    if (!$this->email)
      $this->email = "{$attributes['id']}@{$client_id}.net";
    parent::__construct($config);
  }
  public function registration($client_id)
  {
    if (User::find()->where(['email' => $this->email])->exists()) {
      Yii::$app->getSession()->setFlash('error', [
        Yii::t('app', 
          'User with {email} have been exist, but not linked to {client}. ' . 
          'Try to login with other social network or with name and password.', [
          'email' => $this->email,
          'client' => $client_id,
        ]),
      ]);
    } else {
      if(User::find()->where(['name' => $this->name])->exists()) {
        Yii::$app->getSession()->setFlash('error', [
          Yii::t('app', 
            'A user named {name} already exists. ' .
            'Try logging in if you have registered before.', [
            'name' => $this->name,
          ]),
        ]);
      } else {
        $password = Yii::$app->security->generateRandomString(6);
        $user = new User([
          'name' => $this->name,
          'email' => $this->email,
          'password' => $password,
          'status' => User::STATUS_ACTIVE,
        ]);
        $user->generateAuthKey();
        $user->generatePasswordResetToken();
        $transaction = $user->getDb()->beginTransaction();
        if ($user->save()) {
          if ($this->makeLink($client_id, $user->id)) {
            $transaction->commit();
            Yii::$app->user->login($user);
          } else {
            throw new InvalidValueException($this->showErrors($this->_link)); 
          }
        } else {
          throw new InvalidValueException($this->showErrors($user)); 
        }
      }
    }
  }
  public function makeLink($client_id, $user_id)
  {
    $this->_link = new SocialLink([
      'user_id' => $user_id,
      'source' => $client_id,
      'source_id' => $this->id,
    ]);
    return $this->_link->save();
  }
  /**
  * Show $model errors in dev mode
  * @param object model
  * @return string errors message 
  */   
  private function showErrors($model)
  {
    $out = 'Can\'t save ' . $model->tableName() . "\n";
    foreach($model->getErrors() as $field => $messages) {
      $out .= "«{$field}»\n";
      foreach($messages as $message) {
        $out .= "{$message}\n";
      }
      $out .= "\n";
    }
    return $out;
  }
}

Теперь действие авторизации в контроллере frontend/controllers/SiteController.php стало проще и понятнее, а значит мы достигли цели :).

  public function onAuthSuccess($client)
  {
    $social_contact = new SocialContact($client);
    $social_link = SocialLink::find()->where([
      'source' => $client->getId(),
      'source_id' => $social_contact->id,
    ])->one();
    if (Yii::$app->user->isGuest) {
      if ($social_link) { // authorization
        Yii::$app->user->login($social_link->user);
      } else { // registration
        $social_contact->registration($client->getId());
      }
    } else { // the user is already registered
      if (!$social_link) { // add external service of authentification
        $social_contact->makeLink($client->getId(), Yii::$app->user->id);
      }
    }
  }

Заключение

Полная версия с аватаром в расширении sergmoro1/yii2-user.

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

Только авторизованные посетители могут оставлять комментарии. Пожалуйста, войдите или пройдите регистрацию.