С помощью модулей yii-user и rights легко внедрить функционал регистрации и авторизации с контролем доступа на основе ролей в приложение Yii. Расширение EAuth позволяет добавить к этому функционалу возможность аутентификации пользователей с помощью аккаунтов других сайтов (сервисов), что делает приложение более гибким и удобным для пользователей.
Принцип работы
В расширении EAuth для каждого сервиса созданы специальные классы-провайдеры, благодаря которым реализуется единый метод аутентификации, не зависящий от используемого пользователем сервиса. Это значительно упрощает внедрение данного функционала в приложение.
EAuth предоставляет идентификатор пользователя id, его имя name и идентификатор сервиса serviceName, а вы уже сами решаете как использовать эти данные для аутентификации пользователя в своём приложении. Для связки user+rights+eauth я реализовал следующую логику:
Теперь подробнее:
- При использовании пользователем стороннего сервиса для входа на сайт мы получаем его id, name и serviceName и проверяем в базе данных наличие записи с такими данными:
- если она есть, значит у пользователя есть аккаунт (он уже регистрировался), с которым эта запись связана. Аутентифицируем пользователя с помощью данных этого аккаунта и присваиваем ему роль Authenticated. Получается, что в этом случае полученные от сервиса данные используются только для поиска аккаунта зарегистрированного пользователя.
- если же такой записи нет, то аутентифицируем пользователя путём сохранения в переменные сессии полученных от сервиса данных. При этом в базу ничего не записывается, так как для создания в БД полноценного аккаунта (модуля user) как минимум нужны логин и пароль. Приложение «забудет» этого пользователя, как только истечёт время жизни сессии или он выйдет сам. Для таких пользователей я создал отдельную роль
ServiceAuth
- , которая имеет меньше прав, чем роль Authenticated. Можно, например, разрешить только добавление комментариев и т.п.
- Привязать сервис к аккаунту можно двумя способами. Зарегистрированный пользователь может сделать это на странице своего профиля. Если же пользователь вошёл через сервис и у него ещё нет аккаунта, то он может пройти регистрацию, в результате чего данный сервис и вновь созданный аккаунт будут связаны автоматически.
Установка
Прежде всего, установите модули yii-user и rights.
EAuth использует протоколы OpenID и OAuth 1.0/2.0, поэтому сперва нужно установить расширения loid и eoauth, которые реализуют поддержку этих протоколов. Затем скачайте EAuth и, как и предыдущие два расширения, поместите его в папку protected/extensions. В итоге у вас должна получиться такая структура папок:
-/extensions
—/eauth
—/eoauth
—/lightopenid
-/modules
—/rights
—/user
Для работы этих трёх расширений в файл конфигурации protected/config/main.php необходимо внести соответствующие изменения, которые с учётом настроек для модулей yii-user и rights будут следующими:
//…
‘application.modules.user.models.*’,
‘application.modules.user.components.*’,
‘application.modules.rights.*’,
‘application.modules.rights.models.*’,
‘application.modules.rights.components.*’,
‘ext.eoauth.*’,
‘ext.eoauth.lib.*’,
‘ext.lightopenid.*’,
‘ext.eauth.*’,
‘ext.eauth.services.*’,
),
‘modules’=>array(
‘user’,
‘rights’
),
‘components’=>array(
‘user’=>array(
‘allowAutoLogin’=>true,
‘class’ => ‘RWebUser’,
),
/* Вы используете настройки для своей базы данных */
‘db’=>array(
‘connectionString’ => ‘mysql:host=localhost;dbname=testdrive’,
’emulatePrepare’ => true,
‘username’ => ‘root’,
‘password’ => »,
‘charset’ => ‘utf8’,
‘tablePrefix’ => ‘tbl_’,
),
‘authManager’=>array(
‘class’=>’RDbAuthManager’,
‘defaultRoles’ => array(‘Guest’),
),
‘loid’ => array(
‘class’ => ‘application.extensions.lightopenid.loid’,
),
‘eauth’ => array(
‘class’ => ‘ext.eauth.EAuth’,
// Использовать всплывающее окно вместо перенаправления.
‘popup’ => true,
// Имя компонента кэширования или false для отключения.
// По умолчанию ‘cache’.
‘cache’ => false,
// Время жизни кэша.
‘cacheExpire’ => 0,
// Провайдеры
‘services’ => array(
‘yandex_oauth’ => array(
//register your app here: https://oauth.yandex.ru/client/my
‘class’ => ‘YandexOAuthService’,
‘client_id’ => ‘…’,
‘client_secret’ => ‘…’,
‘title’ => ‘Yandex (OAuth)’,
),
‘google_oauth’ => array(
//register your app here: https://code.google.com/apis/console/
‘class’ => ‘GoogleOAuthService’,
‘client_id’ => ‘…’,
‘client_secret’ => ‘…’,
‘title’ => ‘Google (OAuth)’,
),
‘vkontakte’ => array(
//register your app here: https://vk.com/editapp?act=create&site=1
‘class’ => ‘VKontakteOAuthService’,
‘client_id’ => ‘…’,
‘client_secret’ => ‘…’,
),
),
),
),
Для примера я добавил только три сервиса: vkontakte, yandex и google. Для каждого сервиса OAuth необходимо зарегистрировать приложение по указанным ссылкам, чтобы получить client_id и client_secret. Я покажу как это делается на примере vkontakte.
Перейдя по ссылке https://vk.com/editapp?act=create&site=1, вы попадаете на страницу создания приложения, на которой я заполнил поля следующими данными:
Здесь я указал адрес страницы, которая обрабатывает данные сервиса (можно использовать локальный сервер localhost):
После этого нужно будет ввести код из SMS, а затем перейти на вкладку «Настройки», где вы увидите «ID приложения» и «Защищённый ключ», то есть client_id и client_secret соответственно.
База данных
Кроме таблиц модулей user и rights нужно создать в базе данных таблицу, которая будет использоваться для связывания аккаунтов с сервисами. Я назвал её tbl_service.
Поле identity является первичным ключом и в него мы будем записывать id пользователя, полученный от сервиса. В поле service_name переменную serviceName. Ну а в user_id — id пользователя из таблицы tbl_users.
Для удобства и возможности использовать ActiveRecord я создал модель protected/models/Service.php:
<p>class Service extends CActiveRecord<br ?—> {
public function tableName()
{
return ‘{{service}}’;
}
public function relations()
{
return array(
‘user’ => array(self::BELONGS_TO, ‘User’, ‘user_id’),
);
}
public static function model($className=__CLASS__)
{
return parent::model($className);
}
}
Использование
Добавьте виджет EAuth в представление protected/modules/user/views/user/ligin.php, в котором содержится форма входа yii-user.
echo ‘
Yii::app()->user->getFlash(‘error’).’
‘;
}
?>
Используйте для входа аккаунт одного из указанных ниже сайтов:
$this->widget(‘ext.eauth.EAuthWidget’, array(
‘action’ => ‘login’
));
?>
Полученные от формы входа или выбранного сервиса данные обрабатываются действием actionLogin контроллера LoginController. Поэтому нужно изменить его следующим образом (в файле protected/modules/user/controllers/LoginController.php):
{
$serviceName = Yii::app()->request->getQuery(‘service’);
if (isset($serviceName))
{
$eauth = Yii::app()->eauth->getIdentity($serviceName);
$eauth->redirectUrl = Yii::app()->user->returnUrl;
$eauth->cancelUrl = $this->createAbsoluteUrl(‘user/login’);
try
{
if ($eauth->authenticate())
{
$identity = new ServiceUserIdentity($eauth);
// успешная аутентификация
if ($identity->authenticate())
{
Yii::app()->user->login($identity);
$eauth->redirect();
}
else
{
// закрытие popup-окна
$eauth->cancel();
}
}
$this->redirect(array(‘user/login’));
}
catch (EAuthException $e)
{
Yii::app()->user->setFlash(‘error’,
‘EAuthException: ‘.$e->getMessage());
$eauth->redirect($eauth->getCancelUrl());
}
}
elseif (Yii::app()->user->isGuest)
{
$model=new UserLogin;
// collect user input data
if(isset($_POST[‘UserLogin’]))
{
$model->attributes=$_POST[‘UserLogin’];
// validate user input and redirect to previous page if valid
if($model->validate())
{
$this->lastViset();
if (Yii::app()->getBaseUrl().»/index.php» === Yii::app()->user->returnUrl)
$this->redirect(Yii::app()->controller->module->returnUrl);
else
$this->redirect(Yii::app()->user->returnUrl);
}
}
// display the login form
$this->render(‘/user/login’,array(‘model’=>$model));
} else
$this->redirect(Yii::app()->controller->module->returnUrl);
}
Логика аутентификации с помощью сервисов описана в классе ServiceUserIdentity. Создайте файл protected/components/ServiceUserIdentity.php:
<p>class ServiceUserIdentity extends CBaseUserIdentity {</p>
<p> const ERROR_NOT_AUTHENTICATED = 3;</p>
<p> protected $service;<br ?—> protected $id;
protected $name;
public function __construct($service) {
$this->service = $service;
}
public function authenticate() {
$serviceModel = Service::model()->findByPk($this->service->id);
/* Если в таблице tbl_service нет записи с таким id,
значит сервис не привязан к аккаунту. */
if($serviceModel === null){
if ($this->service->isAuthenticated) {
$this->id = $this->service->id;
$this->name = $this->service->getAttribute(‘name’);
$this->setState(‘service’, $this->service->serviceName);
$this->errorCode = self::ERROR_NONE;
}
else {
$this->errorCode = self::ERROR_NOT_AUTHENTICATED;
}
}
/* Если запись есть, то используем данные из
таблицы tbl_users, используя связь в модели Service */
else {
$this->id = $serviceModel->user->id;
$this->name = $serviceModel->user->username;
$this->errorCode = self::ERROR_NONE;
}
return !$this->errorCode;
}
public function getId() {
return $this->id;
}
public function getName() {
return $this->name;
}
}
Если вы сейчас попробуете войти с помощью какого-нибудь сервиса, то получите ошибку. Она возникает из-за метода updateSession() модуля user. Чтобы исправить это, измените метод afterLogin() в файле protected/modules/user/components/WebUser.php:
{
parent::afterLogin($fromCookie);
if(!Yii::app()->user->getState(‘service’)) {
$this->updateSession();
}
}
Как видите, узнать каким образом аутентифицирован пользователь можно проверяя наличие значения service, которое устанавливается только при входе с помощью стороннего сервиса.
Такая проверка будет нужна неоднократно, поэтому в расширении EAuth можно создать метод по типу Yii::app()->user->isGuest. В файле protected/extensions/eauth/EAuth.php добавьте:
return Yii::app()->user->getState(‘service’)!==null;
}
Теперь можно выполнять проверку следующим образом:
Управление доступом
Итак, если пользователь вошёл с помощью сервиса, который не связан ни с одним аккаунтом, то ему должна быть присвоена роль ServiceAuth. По описанной мной логике она имеет больше прав, чем роль Guest, но меньше, чем Authenticated. Какие операции разрешать для этой роли решайте сами. Сначала создайте эту роль на странице модуля rights:
Но с присвоением этой роли возникает проблема, потому что такой пользователь не сохранён в базе данных. Решить эту проблему можно, указав ServiceAuth, как роль по умолчанию:
В этом случае по умолчанию пользователь будет иметь право на выполнение операций, разрешённых как для роли Guest, так и для роли ServiceAuth. Их можно разграничить, добавив для ServiceAuth бизнес-правило как на скриншоте выше.
Также необходимо запретить доступ для роли ServiceAuth к некоторым страницам модуля user (профиль, редактирование профиля и т.д.). Для этого на странице …/index.php?r=rights/authItem/generate выберите соответствующие контроллеры модуля user:
В результате будут сгенерированы задачи, что позволит предоставлять доступ сразу ко всем действиям этих контроллеров. Присвойте роли Authenticated только задачу User.Profile.*, так как User.ProfileField.* и User.Admin.* должны быть разрешены только для администратора:
Не забудьте в каждом из этих контроллеров изменить наследуемый класс на RController, метод filters() изменить на (если его нет, то добавьте)
а метод accessRules() вообще удалите.
Привязка сервиса к аккаунту
В профиле пользователя
Сначала я покажу как вывести уже связанные с аккаунтом сервисы на странице профиля, то есть записи из таблицы tbl_service с user_id текущего пользователя. Для этой цели я немного изменил виджет Eauth. В файле виджета protected/extensions/eauth/EAuthWidget.php добавьте новую переменную:
public $view = ‘auth’;
…
Здесь же в методе run() вставьте эту переменную в функцию render():
parent::run();
$this->registerAssets();
$this->render($this->view, array(
‘id’ => $this->getId(),
‘services’ => $this->services,
‘action’ => $this->action,
));
}
Создайте представление protected/extensions/eauth/views/linkedServices.php, которое будет выводить связанные с аккаунтом сервисы:
<codelang=»php»>
-
foreach ($services as $name => $service) {
- ‘;
$html = ‘‘;
$html .= ‘‘ . $service->title
.CHtml::link(
‘удалить’,
array($action, ‘service’ => $name),
array(‘class’ => ‘auth-link ‘ . $service->id,)
)
.’‘;
echo $html;
echo ‘
- echo ‘
‘;
}
?>
Для нормального отображения я внёс некоторые изменения в файл стилей виджета protected/extensions/eauth/assets/css/auth.css:
float: left;
margin: 0 1em 0 0;
width: 58px;
text-align: center;
}
.auth-service /*.auth-link*/ .auth-icon {
margin: 0 auto;
}
В методе actionProfile контроллера ProfileController нужно добавить выборку записей из таблицы tbl_service по идентификатору пользователя и передать их в представление:
{
$model = $this->loadUser();
$serviceModel = Service::model()->findAllByAttributes(array(
‘user_id’=>$model->id
));
$services = array();
foreach ($serviceModel as $row) {
$services[] = $row->service_name;
}
$this->render(‘profile’,array(
‘model’=>$model,
‘profile’=>$model->profile,
‘services’=>$services,
));
}
Заметьте, что в представление передаётся массив $services, содержащий только названия сервисов.
В представление protected/modules/user/views/profile/profile.php добавляем два виджета EAuth: один выводит уже привязанные сервисы, а второй — сервисы, которые можно привязать к аккаунту.
Связанные с аккаунтом сервисы::
$this->widget(‘ext.eauth.EAuthWidget’, array(
‘action’ => ‘deleteService’,
‘view’=>’linkedServices’,
‘popup’=>false,
‘predefinedServices’=>$services
));
?>
Выберите сервис для привязки к аккаунту:
$allServices = array_keys(Yii::app()->eauth->getServices());
$this->widget(‘ext.eauth.EAuthWidget’, array(
‘action’ => ‘login/login’,
‘predefinedServices’ => array_diff($allServices,$services)));
?>
Свойство ‘predefinedServices позволяет задать сервисы, которые должны быть отображены виджетом. В первом случае в этом свойстве указан полученный от контроллера массив. Во втором же необходимо, чтобы выводились сервисы, которые ещё не связаны с аккаунтом, то есть все, кроме сервисов из массива $services. Метод getServices() расширения EAuth возвращает массив определённых в конфиге сервисов и их настройки. Ключами этого массива являются названия сервисов, поэтому мы извлекаем их функцией array_keys в массив $allServices, а затем находим те сервисы массива $allServices, которых нет в массиве $services.
В свойстве ‘action’ указаны действия контроллера ProfileController для удаления и создания связи. Если сервис связан с аккаунтом, то пользователь сможет выполнять вход с помощью этого сервиса без использования логина и пароля. Действие удаления нужно добавить в protected/modules/user/controllers/ProfileController.php:
public function actionDeleteService(){
$service = Service::model()->findByAttributes(array(
‘service_name’=>Yii::app()->request->getQuery(‘service’),
‘user_id’=>Yii::app()->user->id,
));
$service->delete();
$this->redirect(array(‘/user/profile’));
}
А добавляться запись будет в действии actionLogin контроллера LoginController. Его нужно изменить следующим образом:
{
$serviceName = Yii::app()->request->getQuery(‘service’);
if (isset($serviceName))
{
$eauth = Yii::app()->eauth->getIdentity($serviceName);
$eauth->redirectUrl = Yii::app()->user->returnUrl;
$eauth->cancelUrl = $this->createAbsoluteUrl(‘user/login’);
try
{
if ($eauth->authenticate())
{
$identity = new ServiceUserIdentity($eauth);
// успешная аутентификация
if ($identity->authenticate())
{
if(Yii::app()->user->isGuest){
Yii::app()->user->login($identity);
$eauth->redirect();
}
else
{
$eauth->redirectUrl = $this->createAbsoluteUrl(‘/user/profile’);
$eauth->cancelUrl = $this->createAbsoluteUrl(‘/user/profile’);
$service = new Service();
$service->identity = $eauth->id;
$service->service_name = $eauth->serviceName;
$service->user_id = Yii::app()->user->id;
if ($service->save()) {
$eauth->redirect();
}
}
}
else
{
// закрытие popup-окна
$eauth->cancel();
}
}
$this->redirect(array(‘user/login’));
}
catch (EAuthException $e)
{
Yii::app()->user->setFlash(‘error’,
‘EAuthException: ‘.$e->getMessage());
$eauth->redirect($eauth->getCancelUrl());
}
}
elseif (Yii::app()->user->isGuest)
{
$model=new UserLogin;
// collect user input data
if(isset($_POST[‘UserLogin’]))
{
$model->attributes=$_POST[‘UserLogin’];
// validate user input and redirect to previous page if valid
if($model->validate())
{
$this->lastViset();
if (Yii::app()->getBaseUrl().»/index.php» === Yii::app()->user->returnUrl)
$this->redirect(Yii::app()->controller->module->returnUrl);
else
$this->redirect(Yii::app()->user->returnUrl);
}
}
// display the login form
$this->render(‘/user/login’,array(‘model’=>$model));
} else
$this->redirect(Yii::app()->controller->module->returnUrl);
}
Привязка сервиса при регистрации
Вошедший через сервис пользователь (с ролью ServiceAuth) имеет возможность пройти регистрацию, в результате чего этот сервис должен быть привязан к созданному аккаунту. Сначала нужно сделать так, чтобы при переходе на страницу регистрации пользователь не перенаправлялся на страницу профиля (она будет недоступна). Для этого внесите следующие изменения в действие actionRegistration контроллера protected/modules/user/controllers/RegistrationController.php:
if (Yii::app()->user->id && !Yii::app()->user->getState(‘service’)) {
$this->redirect(Yii::app()->controller->module->profileUrl);
} else {
…
В этом же действии:
if ($model->save()) {
$profile->user_id=$model->id;
$profile->save();
// нужно вставить этот фрагмент
if(Yii::app()->user->getState(‘service’)){
$this->linkServiceToUser($model->id);
}
…
Также в этот контроллер добавьте метод linkServiceToUser():
$service = new Service;
$service->identity = Yii::app()->user->id;
$service->service_name = Yii::app()->user->service;
$service->user_id = $userID;
$service->save();
}
Осталось лишь изменить видимость ссылок на страницы профиля и регистрации в главном шаблоне protected/views/layouts/main.php:
array(‘url’=>Yii::app()->getModule(‘user’)->registrationUrl,
‘label’=>Yii::app()->getModule(‘user’)->t(«Register»),
‘visible’=>Yii::app()->user->isGuest || Yii::app()->user->getState(‘service’)),
array(‘url’=>Yii::app()->getModule(‘user’)->profileUrl,
‘label’=>Yii::app()->getModule(‘user’)->t(«Profile»),
‘visible’=>!Yii::app()->user->isGuest && !Yii::app()->user->getState(‘service’)),
…
Если я что-то упустил или у вас есть какие-либо замечания, то оставляйте их в комментариях.
Добавить комментарий