Как «подружить» сервис Loginza и фреймворк Yii

Владимир | | PHP, Web разработка, Yii.

loginza yii

Постоянные читатели этого блога, наверное, помнят, что и о фреймворке Yii, и о сервисе аутентификации Loginza я раньше рассказывал. Найти эти статьи несложно: Yii, Loginza.

Но в статье о Loginza, речь шла о протоколе обмена данными с сервисом, а вопрос аутентификации остался «за бортом». Я, конечно, объяснил когда нужно создавать сессию, но на практике этого явно недостаточно 😉

В этой статье я постараюсь исправить этот недостаток и покажу пример аутентификации пользователя с помощью сервиса Loginza.

Прежде всего, определим требования к такой системе.

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

2) Должны поддерживаться стандартные средства Yii для работы с пользователями. Речь о компоненте Yii::app()->user (CWebUser).

3) Завершение сеанса (Yii::app()->user->logout()) должно работать независимо от того каким образом была выполнена аутентификация.

4) Если посетитель впервые зашел на сайт и успешно прошел аутентификацию с помощью Loginza, для него нужно автоматически создать аккаунт.

5) Нужна возможность удобно показывать ссылку «Войти с помощью Loginza» на любой странице сайта (виджет).

6) После аутентификации пользователь должен вернуться на исходную страницу.

Чтобы лучше представить фронт работ, рассмотрим алгоритм аутентификации фреймворка Yii.

1. Посетитель кликает по кнопке «Войти с помощью Loginza».

2. Появляется виджет в котором пользователь выбирает сервис аутентификации.

3. Loginza проводит аутентификацию и возвращает посетителя на наш сайт. При этом в массиве $_POST передается token, с помощью которого можно получить результаты аутентификации пользователя.

4. Наше приложение должно.

4.1. Получить данные от сервиса Loginza.

4.2. Создать пользователя (экземпляр класса, реализующий интерфейс IUserIdentity). Этот класс содержит метод authenticate в котором мы проверим, существует ли данный пользователь в БД, если нет – создадим его, и установим данные, которые необходимы для работы класса CWebUser.

4.3. Выполнить аутентификацию. Т.е. вызвать Yii::app()->user->login. При этом в первом параметре метода login мы передаем экземпляр класса, реализующего IUserIdentity (который мы создали на предыдущем этапе). Таким образом, мы обеспечим нормальную работу нашего класса со всеми остальными компонентами фреймворка.

Переходим к реализации.

Шаг 1. Создадим виджет, показывающий ссылку «Войти с помощью Loginza».

Он будет состоять из двух файлов:
1) protected/components/LoginzaWidget.php – класс виджета
2) protected/components/views/loginzaWidget.php – представление.

Рассмотрим класс виджета.

<?php
class LoginzaWidget extends CWidget {
	//параметры по-умолчанию
	private $params = array(
		'widget_url'=>'https://s3-eu-west-1.amazonaws.com/s1.loginza.ru/js/widget.js',
		'token_url'=>'https://loginza.ru/api/widget?token_url=',
		'return_url'=>'',
		'link_anchor'=>'Войти с помощью Loginza',
		'logout_url'=>'',
		'css_class'=>'loginza',
		'providers_set'=>array('vkontakte', 'facebook', 'twitter', 'loginza'
            , 'myopenid', 'webmoney', 'rambler', 'flickr', 'lastfm', 'openid'
            , 'mailru', 'verisign', 'aol', 'steam', 'google', 'yandex'
            , 'mailruapi'),
	);

	/**
	 * Этот метод подключает JS скрипт и загружает представление
	 */
    public function run() {
        //подключаем JS скрипт
        Yii::app()->clientScript->registerScriptFile(
				$this->params['widget_url']
                , CClientScript::POS_END);
        $this->render('loginzaWidget', $this->params);
    }

	/**
	 * Установка параметров
	 * @param array $params массив с параметрами
	 */
	public function setParams($params) {
		$this->params = array_merge($this->params, $params);
	}
}

Здесь мы создаем массив с дефолтными параметрами, в котором указываем настройки, необходимые для работы с сервисом Loginza. Подробно на них останавливаться не будем, т.к. они рассмотрены в предыдущей статье. Но обратите внимание, что обязательно нужно установить два параметра: return_url и logout_url.

В первом (return_url) нужно указать адрес, на который должен быть перенаправлен пользователь после аутентификации на Loginza.

Во втором (logout_url) – адрес метода, выполняющего завершение сессии.

Эти методы выполняют непосредственно аутентификацию пользователя в нашем приложении, и логично, что виджет о них ничего не знает.

Таким образом, подключить виджет можно так.

<?php  $this->widget('application.components.LoginzaWidget', array(
	'params'=>array(
		'return_url'=>'site/loginzalogin',
		'logout_url'=>'site/logout',
		'providers_set'=>array('google','vkontakte','facebook','twitter','rambler','openid','mailru','yandex','mailruapi'),
	),
)); ?>

В представлении (protected/components/views/loginzaWidget.php) мы проверяем, аутентифицирован ли пользователь и в зависимости от результата выводим ссылку «Войти с помощью Loginza» или «Выход».

<?php if (Yii::app()->user->isGuest) {
	$url = $token_url.Yii::app()->createAbsoluteUrl($return_url
			, array('providers_set'=>implode(',',$providers_set)));
	echo CHtml::link($link_anchor, $url, array('class'=>$css_class));
} else {
	$anchor = 'Выйти ('.Yii::app()->user->getName().')';
	echo CHtml::link($anchor, array($logout_url));
}
?>

Шаг 2. Напишем метод, выполняющий аутентификацию посетителя.

Я добавил этот метод в класс SiteController, который автоматически создается утилитой yiic при создании приложения.

public function actionLoginzaLogin() {
	//проверяем, пришел ли token
	if (isset($_POST['token'])) {
		$loginza = new LoginzaModel();
		$loginza->setAttributes($_POST);
		$loginza->getAuthData();
		if ($loginza->validate() && $loginza->login()) {
			//возвращаем пользователя на ту страницу на которой он
			//находился перед аутентификацией
			$this->redirect(Yii::app()->user->returnUrl);
		}
		else {
			//сообщение об ошибке
			$this->render('loginzaerror');
		}
	}
	else {
		//если этот метод вызван напрямую (без указания token)
		$this->redirect(Yii::app()->homeUrl, true);
	}
}

Этот метод мы указали в параметре return_url при настройке виджета, таким образом, именно на него придет редирект от сервиса Loginza.

Мы проверяем наличие параметра token в массиве $_POST и если он найден, продолжаем процедуру аутентификации.

От Loginza мы получаем данные о результатах аутентификации, и проверить их будет совсем не лишним. Мы, конечно, можем написать весь код самостоятельно, но гораздо удобнее использовать встроенные средства фреймворка.

Шаг 3. Создаем модель, выполняющую обработку данных от Loginza.

Модель будет называться LoginzaModel, но прежде чем переходить к ее детальному рассмотрению, определимся, какие данные о пользователе нужно хранить в БД.

Loginza передает в обязательно порядке два параметра: identity и provider (именно они позволяют однозначно идентифицировать пользователя), остальные зависят от выбранного сервиса аутентификации.

Кроме того, полезно будет хранить email пользователя и его имя. К сожалению, в зависимости от сервиса вы можете получить эти данные, а можете и не получить 🙂

Теперь рассмотрим сам класс.

<?php
class LoginzaModel extends CModel {

	public $identity;
	public $provider;
	public $email;
	public $full_name;
	public $token;
	public $error_type;
	public $error_message;

	private $loginzaAuthUrl = 'http://loginza.ru/api/authinfo?token=';

	public function rules() {
		return array(
			array('identity,provider,token', 'required'),
			array('email', 'email'),
			array('identity,provider,email', 'length', 'max'=>255),
			array('full_name', 'length', 'max'=>55),
		);
	}

	public function attributeLabels() {
		return array(
			'identity'=>'Идентификатор сервиса аутентификации',
			'provider'=>'Сервис аутентификации',
			'email'=>'eMail',
			'full_name'=>'Имя',
		);
	}

	/**
	 * Получение данных от сервиса Loginza.
	 * Предварительно нужно установить $this->token
	 * Например, так
	 * $loginza = new LoginzaModel();
	 * $loginza->setAttributes($_POST);
	 */
	public function getAuthData() {
        //получаем данные от сервера Loginza
        $authData = json_decode(
				file_get_contents($this->loginzaAuthUrl.$this->token)
				,true);

		//устанавливаем атрибуты
		//если будут отсутствовать identity и provider, метод validate
		//выдаст ошибку
		$this->setAttributes($authData);
		//full_name находится внутри вложенного массива
		//TODO доделать установку имени для разных сервисов
		$this->full_name = (isset($authData['name']['full_name'])) ? $authData['name']['full_name'] : $authData['identity'];
	}

	/**
	 * Аутентификация посетителя.
	 * @return boolean true - если посетитель аутентифицирован, false - в противном случае.
	 */
	public function login() {
		$identity = new LoginzaUserIdentity();
		if ($identity->authenticate($this)) {
			$duration = 3600*24*30; // 30 days
			Yii::app()->user->login($identity,$duration);
			return true;
		}
		return false;
	}

	public function attributeNames() {
		return array(
			'identity'
			,'provider'
			,'email'
			,'full_name'
			,'token'
			,'error_type'
			,'error_message'
		);
	}
}

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

Метод getAuthData отправляет запрос серверу Loginza, обрабатывает результат и устанавливает значения атрибутам этого класса.

Вы, наверное, заметили, что установка имени пользователя немного не доделана. Дело в том, что сервисы передают эти данные в разных элементах массива. Элемент full_name устанавливает Google. По большому счету, нужно просмотреть данные, которые возвращают остальные сервисы и соответственно устанавливать $this->full_name.

Метод login в случае успешной аутентификации создает сессию для пользователя. В нём мы создаем экземпляр класса LoginzaUserIdentity, который реализует интерфейс IUserIdentity, и передаем его в первом параметре Yii::app()->user->login.

Также LoginzaUserIdentity содержит метод authenticate, который выполняет поиск пользователя в БД и, в случае необходимости, создает новый аккаунт.

В зависимости от результата, метод login возвращает true или false.

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

Шаг 4. Создаем таблицу User

Тут все просто.

Для создания таблицы можно использовать следующий SQL запрос

CREATE  TABLE IF NOT EXISTS `tablename`.`user` (
  `u_id` INT NOT NULL AUTO_INCREMENT ,
  `u_identity` VARCHAR(255) NOT NULL ,
  `u_provider` VARCHAR(255) NOT NULL ,
  `u_email` VARCHAR(255) NULL ,
  `u_full_name` VARCHAR(55) NULL ,
  `u_state` TINYINT(1) NOT NULL ,
  PRIMARY KEY (`u_id`) )
ENGINE = InnoDB;

А классы, необходимые для выполнения CRUD, можно создать с помощью компонента gii.

Вызывается так.
sitename.domen/index.php?r=gii

Предварительно нужно активировать его в настройках приложения (config/main.php)

'modules'=>array(
	'gii'=>array(
		'class'=>'system.gii.GiiModule',
		'password'=>'yourpass',
	),
),

Шаг 5. Создаем класс LoginzaUserIdentity.

Размещаться он будет в protected/components.

Как я уже упоминал, он реализует интерфейс IUserIdentity и, таким образом, фреймворк сможет с ним работать.

<?php
/**
 * Этот класс выполняет аутентификацию и, при необходимости, регистрацию посетителя.
 */
class LoginzaUserIdentity implements IUserIdentity {

	private $id;
	private $name;
	private $isAuthenticated = false;
	private $states = array();

	public function __construct() {
	}

	/**
	 * Аутентификация пользователя.
	 * Этот метод ищет пользователя в БД. Если он не найден, создает нового.
	 * Устанавливает значения атрибутов.
	 *
	 * @param LoginzaModel $loginzaModel модель, содержащая данные от сервиса Loginza
	 * @return boolean true если пользователь найден или создан новый аккаунт, false - если недостаточно данных
	 */
	public function authenticate($loginzaModel = null) {

		if (empty($loginzaModel->identity) || empty($loginzaModel->provider)) {
			return false;
		}
        //сначала проверяем, существует ли такой пользователь в БД
        $criteria=new CDbCriteria;
        $criteria->condition = 'u_identity=:identity AND u_provider=:provider';
        $criteria->params = array(
            ':identity'=>$loginzaModel->identity
            , ':provider'=>$loginzaModel->provider
        );
		$user = User::model()->find($criteria);
		if (null !== $user) {
			//используем существующего пользователя
			$this->id = $user->u_id;
			$this->name = (null != $user->u_full_name) ? $user->u_full_name : $user->u_identity;
		}
		else {
			//создаем нового
			$user = new User();
			$user->u_identity = $loginzaModel->identity;
			$user->u_provider = $loginzaModel->provider;
			$user->u_email = $loginzaModel->email;
			$user->u_full_name = $loginzaModel->full_name;
			$user->save();
			$this->id = $user->u_id;
			$this->name = (null != $user->u_full_name) ? $user->u_full_name : $user->u_identity;
		}
		$this->isAuthenticated = true;
		return true;
	}

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

	public function getIsAuthenticated() {
		return $this->isAuthenticated;
	}

	public function getName() {
		return $this->name;
	}

	public function getPersistentStates() {
		return $this->states;
	}
}

Основную часть работы выполняет метод authenticate. Рассмотрим его подробнее.

Прежде всего, мы проверяем, есть ли у нас необходимые данные для поиска пользователя или создания нового аккаунта. Это атрибуты identity и provider модели.

Затем выполняем поиск пользователя в БД. Модель User у нас создана, поэтому поиск можно выполнить с помощью метода find.
User::model()->find($criteria)
Здесь $criteria – экземпляр CDbCriteria в котором мы установили условие 'u_identity=:identity AND u_provider=:provider'.

Если пользователь найден, устанавливаем значения атрибутов name, id и isAuthenticated. Если нет — создаем нового.

Остальные методы необходимо реализовать, т.к. они объявлены в интерфейсе IUserIdentity. Они возвращают основные данные о текущем пользователе.

Благодаря им обеспечивается совместная работа с классом CWebUser. Например, будут правильно установлены атрибуты
Yii::app()->user->isGuest, Yii::app()->user->name и т.п.

Шаг 6. Обеспечиваем возврат пользователя на исходную страницу.

Т.к. в процессе аутентификации выполняется несколько редиректов, а первый этап выполняется с помощью JS кода, то нам нужно заранее сохранить адрес страницы, на которой находился пользователь. Сделать это проще всего с помощью сессий.

К тому же, есть довольно удобный компонент под названием setReturnUrl.

Он устанавливается как фильтр

public function filters() {
	return array(
		array(
			'ESetReturnUrlFilter - login, loginzalogin, logout',
		),
	);
}

Здесь мы указываем, что этот фильтр не должен применяться к методам login, loginzalogin, logout, т.к. мы должны вернуть пользователя на страницу, на которой он находился до того, как они были вызваны.

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

Source

Рассмотреть каждый мелкий нюанс практически невозможно, статья бы получилась просто огромной 😉 . К тому же, я предполагаю, что у вас есть хотя бы общее представление о фреймворке Yii.

Но, в любом случае, я будут рад ответить на вопросы и обсудить замечания и предложения!

До встречи!

Интересно почитать

Четыре способа увеличения продаж на конкурентном б2б-рынке