vorst.ru - Как подключить авторизацию через социальные сети
Статьи из рубрики access-control

Рефакторинг кода авторизации

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

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

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


Как подключить авторизацию через аккаунт

OAuth2 авторизация с использованием аккаунта

Допустим, что вы уже зарегистрированы в известной социальной сети. То есть вы уже сообщили о себе, все, что считали нужным. Очевидно вам не хочется проходить процедуру еще раз на другом ресурсе.

Авторизация с помощью социальной сети позволяет избежать повторного заполнения формы регистрации. При этом защищенность вашего аккаунта в социальной сети никак не страдает. Вы просто подтверждаете, что вы это вы. Как подключить эту услугу к проекту используя расширение yiisoft/yii2-authclient?


Как определить правило для RBAC

Выполнять действие или нет

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

Если вы Админ, то у вас есть общее разрешение create. Оно дает право добавлять данные к любым моделям. Если вы Автор, то такое разрешение давать не стоит. Автор не может, например, добавить новую рубрику.


Определение разрешений на доступ и ролей для пользователей

Задание доступа только к определенным действиям

Разработка бекенда сайта часто подразумевает ограничение прав при редактировании контента.

Ролевая модель имеет иерархию. Сначала прав не много, потом чуть больше, наконец доступно все. Права (или разрешения) - это константы, которые связаны с конкретной ролью. Если за ролью закреплено разрешение, то действие может быть выполнено.


Как подключить RBAC

Подключение ролевого доступа

В большом количестве задач необходимо в backend-части приложения разрешать одним группам пользователей выполнять только определенные действия. Другим может быть разрешено чуть больше. И так далее, до админа.

Именно распределение прав для групп пользователей и будет рассмотрено в паре-тройке статей. В качестве примера будет использоваться всего две роли.



Поиск



Как подключить авторизацию через аккаунт

OAuth2 авторизация с использованием аккаунта

  • 1. Как подключить авторизацию через аккаунт
  • 2. Рефакторинг кода авторизации

Допустим, что вы уже зарегистрированы в известной социальной сети. То есть вы уже сообщили о себе, все, что считали нужным. Очевидно вам не хочется проходить процедуру еще раз на другом ресурсе.

Авторизация с помощью социальной сети позволяет избежать повторного заполнения формы регистрации. При этом защищенность вашего аккаунта в социальной сети никак не страдает. Вы просто подтверждаете, что вы это вы. Как подключить эту услугу к проекту используя расширение yiisoft/yii2-authclient?


    Поделиться

Когда пользователь пытается войти на сайт с помощью стороннего сервиса происходит запрос данных пользователя на этом сервисе. Данные возвращаются в объекте client.

Стандартный запрос возвращает имя пользователя, а вот email может уже и не передать. В социальной сети вы можете установить флаг в настройках - "Передавать email сторонним сервисам" (или подобный флаг). Но не факт, что так поступят все посетители вашего сайта. И это первая проблема.

Вторая в том, что названия возвращаемых атрибутов разные для разных социальных сетей.

Инкапсуляция получения данных

Можно написать класс common/components/SocialContact.php, в конструктор которого передается объект client. Конструктор определяет, от какой социальной сети вернулся объект и, в зависимости от этого, формирует данные.

Если email не передан, то генерируется нечто похожее на email. Это необходимо для выполнения в дальнейшем процедуры регистрации пользователя.

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;
    public function __construct($client, $config = [])
    {
        $client_id = $client->getId();
        $attributes = $client->getUserAttributes();
        $this->id = (string)$attributes['id'];
        switch ($client_id) {
        case 'yandex' :
            $this->name = $attributes['first_name'] . ' ' . 
                $attributes['last_name'];
            $this->email = isset($attributes['default_email']) 
                ? $attributes['default_email'] 
                : false;
            break;
        case 'facebook' :
            $this->name = $attributes['name'];
            $this->email = isset($attributes['email']) 
                ? $attributes['email'] 
                : false;
            break;
        // cases for other clients
        }
        // if email not setted then make it
        if (!$this->email)
            $this->email = "{$attributes['id']}@{$client_id}.net";
        parent::__construct($config);
    }

Связь между User и социальной сетью

Получив объект от стороннего сервиса и приведя данные к единому виду нужно добавить нового пользователя. При этом новому пользователю присваивается случайный пароль. В дальнейшем имя и пароль будут использоваться для входа. Пользователь никогда не узнает свой пароль и даже может не подозревать, что и email используется не его, а сгенерированный. Почему, тем не менее, все будет работать верно?

Дело в том, что пользователь входит в социальную сеть и этого вполне достаточно, чтобы удостоверить его право на вход на любой другой сервис, если пользователь одобрил вход. Поэтому ни пароль, ни email не имеют значения и служат лишь для выполнения процедуры, работающей при обычном входе.

Пользователь может быть зарегистрирован в разных социальных сетях, поэтому нужна связь между социальной сетью и аккаунтом пользователя на вашем сайте. Создадим модель common/models/SocialLink.php. Где source это "yandex", "vkontakte" и прочее, а source_id это внутренний код социальной сети.

namespace common\models;
use yii\db\ActiveRecord;
use common\models\User;
class SocialLink extends ActiveRecord
{
    public static function tableName()
    {
        return '{{%social_link}}';
    }
    public function rules()
    {
        return [
            [['user_id', 'source', 'source_id'], 'required'],
            ['user_id', 'integer'],
            [['source', 'source_id'], 'string', 'max'=>255],
        ];
    }
    public function getUser()
    {
        return User::findOne($this->user_id);
    }
}

И таблицу.

use yii\db\Migration;
class m180608_123330_create_social_link extends Migration
{
    public $table = '{{%social_link}}';
    public function up()
    {
        $tableOptions = null;
        if ($this->db->driverName === 'mysql') {
            $tableOptions = 'CHARACTER SET utf8 COLLATE utf8_unicode_ci ENGINE=InnoDB';
        }
        $this->createTable($this->table, [
            'id' => $this->primaryKey(),
            'user_id' => $this->integer()->notNull(),
            'source' => $this->string(255)->notNull(),
            'source_id' => $this->string(255)->notNull(),
        ], $tableOptions);
        $this->addForeignKey ('FK_social_link_user', 
            $this->table, 'user_id', '{{%user}}', 'id', 'CASCADE');
    }
    public function down()
    {
        $this->dropTable($this->table);
    }
}

User registration

Добавим процедуру регистрации в класс common/components/SocialContact.php. Если пользователь пытается войти с помощью социальной сети, а он уже прошел процедуру обычной регистрации, то выдаем сообщение, что надо бы использовать логин и пароль, указанные при регистрации. Иначе создаем нового пользователя и связываем его с социальной сетью. Если что-то не получается, выдаем сообщение об ошибке.

class SocialContact extends BaseObject
{
    ...
    public function registration($client_id)
    {
        if (User::find()->where(['email' => $this->email])->exists()) {
            Yii::$app->getSession()->setFlash('error', [
                Yii::t('app', 'User with {email} for {client} have been exist, ' .
                'but not linked to each other. Try to login with name and password.', [
                    'email' => $this->email,
                    'client' => $client_id,
                ]),
            ]);
        } 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()) {
                $social_link = new SocialLink([
                    'user_id' => $user->id,
                    'source' => $client_id,
                    'source_id' => $this->id,
                ]);
                if ($social_link->save()) {
                    $transaction->commit();
                    Yii::$app->user->login($user);
                } else {
                    throw new InvalidValueException($this->showErrors($social_link)); 
                }
            } else {
                throw new InvalidValueException($this->showErrors($user)); 
            }
        }
    }
    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;
    }
}

Controller

Действие в контроллере frontend/controllers/SiteController.php.

class SiteController extends Controller
{
    public function actions()
    {
        return [
            'auth' => [
                'class' => 'yii\authclient\AuthAction',
                'successCallback' => [$this, 'onAuthSuccess'],
            ],
        ],
    }
    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_link = new SocialLink([
                    'user_id' => Yii::$app->user->id,
                    'source' => $client->getId(),
                    'source_id' => $social_contact->id,
                ]);
                $social_link->save();
            }
        }
    }

Регистрация приложений

Когда отправляется запрос стороннему сервису (социальной сети), с просьбой сообщить данные пользователя, можно представить, что ответ присылает приложение. Приложения создаются в каждой социальной сети по разному. Например для сети ВКонтакте смотрите здесь. Получив два параметра - идентификатор и секретный ключ приложения, нужно зарегистрировать их в конфигурации common/config/main.php. По Id и секрету приложение определяет, что ваш сайт имеет право на запрос данных пользователя.

<?php
return [
  'components' => [
    'authClientCollection' => [
      'class' => 'yii\authclient\Collection',
      'clients' => [
        'yandex' => [
          'class' => 'yii\authclient\clients\Yandex',
          'clientId' => 'yandex application id',
          'clientSecret' => 'yandex client secret string',
        ],
        'vkontakte' => [
          'class' => 'yii\authclient\clients\VKontakte',
          'clientId' => 'vk application id',
          'clientSecret' => 'vk client secret string',
        ],

Widget

Осталось разместить ссылки в форме входа. Наряду с обычным вариантом входа на сайт, предложить вход с помощью ссылок на социальные сети. Те, что зарегистрированы в файле конфигурации. Ссылки размещает виджет frontend/widgets/SocialCredentials.php, используя информацию о зарегистрированных приложениях.

namespace frontend\widgets; 
use yii\base\Widget; 
class SocialCredentials extends Widget 
{ 
    public $view = 'socialCredentials'; 
    public $credentials = []; 
    public $icons = []; 
    public function init() { 
        parent::init(); 
        if(!$this->credentials) 
            $this->credentials = \Yii::$app->get('authClientCollection')->clients; 
        if(!$this->icons) 
            $this->icons = \Yii::$app->params['icons']; 
    } 
    public function run() 
    { 
        echo $this->render($this->view, [ 
          'credentials' => $this->credentials, 
          'icons' => $this->icons, 
        ]); 
    } 
}

Ссылки удобно выводить с помощью иконок. Например Font Awesome. Можно определить соответствие социальных сетей и иконок в параметрах при вызове виджета или в файле frontend/config/params.php.

<?php   
return [
'icons' => [ 
    'yandex' => 'yandex', 
    'vkontakte' => 'vk', 
    'github' => 'github', 
    'google' => 'google-plus-g', 
  ],

Вариант представления frontend/widgets/views/socialCredentials.php используемый на данном сайте.

<?php
use yii\helpers\Url;
?>
<p><?= \Yii::t('app', 'You can log in using the social network.') ?></p>
<ul class="list-inline">
<?php foreach($credentials as $id => $client): ?>
    <span class="fa-stack fa-lg">
        <li class="fa fa-circle fa-stack-1x">
            <a href="<?= Url::to(['site/auth', 'authclient' => $id]) ?>" 
                title="<?= \Yii::t('app', 'Login with') . ' ' . ucfirst($client->id) ?>">
                <i class="fa fa-circle fa-stack-2x"></i>
                <i class="fab fa-<?= $icons[$id] ?> fa-stack-1x fa-inverse"></i>
            </a>
        </li>
    </span>
<?php endforeach; ?>
</ul>

Как это работает на данном сайте.

Заключение

В конструкторе класса common/components/SocialContacts.php присваиваются значения переменным, в зависимости от социальной сети. Если возникнет необходимость добавить еще одну, не определенную еще, социальную сеть, то придется переписывать класс. Попробуем переписать конструктор так, чтобы новые возможности не приводили к изменениям. Но об этом в следующей статье.

При подготовке статьи использовано руководство Quick start расширения yiisoft/yii2-authclient. Код, приведенный в статье, используется в расширении yii2-user.

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

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