В CMS 1C-Bitrix вниманию разработчикам представлено четыре системных компонента для реализации функционалов авторизации, смены пароля и регистрации рядовых пользователей системы (system.auth.*), но отсутствует официальная документация оных. В этой статье вы узнаете ограничения и недостатки использования этих компонентов, почему следует использовать именно их, каким образом лучше их использовать и, возможно, сделаете некоторые выводы о том, почему документации нет.
По умолчанию для авторизации и регистрации используются старые компоненты 1.0, использование которых нежелательно. Исправить это можно, отметив галку «Использовать Компоненты 2.0 для авторизации и регистрации» в настройках главного модуля или однократно выполнив код:
Ознакомившись с описанием порядка выполнения страницы мы видим, что на шаге 1.13 (еще до вывода header.php из шаблона сайта) идет проверка прав доступа уровня 1 и если она не пройдена, то выводится форма авторизации. Разберем это подробнее.
Под вызовом формы регистрации подразумеваются следующие действия:
— порядок выполнения страницы продолжает выполнятся без изменений вплоть до пункта 3 «Тело страницы», включая выполнение header.php,
— вместо «Тела страницы» в зависимости от переданных в $_REQUEST переменных подключается один из системных компонентов system.auth.* (конкретно system.auth.authorize если $_REQUEST пустой), в который передаются особые параметры (об этом ниже),
— порядок выполнения страницы продолжает выполнятся без изменений, включая выполнение footer.php.
Это позволяет реализовать собственный дизайн (шаблон) для компонента system.auth.authorize, органично вписывающийся в шаблон сайта между шапкой и подвалом.
Проверка прав доступа «уровня 1» инициирует вызов формы авторизации в следующих случаях:
— если глобальная константа NEED_AUTH определена и равна true,
— если у пользователя (в том числе незарегистрированного) недостаточно прав для чтения запрошенного им файла.
Права на чтение файлов и разделов для разных групп пользователей формируются при редактировании оных административной панелью в служебных файлах .access.php. Каждый файл описывает уровни доступа к файлам и каталогам, расположенным на одном с ним уровне. Возможно создание файла .access.php для каждого каталога в публичной части сайта, каждый последующий такой файл дополняет правила доступа по цепочке каталогов. Пример такого файла в корне сайта:
Такое распределение прав позволяет буквально «на месте» запрашивать логин и пароль пользователя в случаях когда он переходит без активной авторизации в каталог, функционал которого требует авторизацию. Такое часто случается, если пользователь держит в избранном ссылку не на корень сайта, а на конкретный раздел, с которым больше всего работает. В результате пользователь вводит логин и пароль не уходя с целевой страницы, те отправляются на ее URL, вместо страницы отрабатывает системный компонент обрабатывая полученные данные либо сыпет ошибку либо авторизует и перезагружает страницу. Пользователь доволен, ему не надо совершать лишний клик для перехода в нужный раздел. Программист доволен, ему не нужно думать о редиректах на /auth/ и $backurl.
Использовать же глобальную константу NEED_AUTH рекомендую только в специфических случаях, одним из которых является непосредственно страница авторизации в стандартной поставке демо-сайта по адресу /auth/index.php:
Этот скрипт передает управление ядру Битрикса, определив перед этим константу NEED_AUTH. Битрикс, в случае если пользователь не авторизован, подключает шапку + системный компонент (авторизации) + подвал и умирает, не позволяя скрипту выполняться дальше второй строчки. В случае же если пользователь авторизован — ему выводится приветственное сообщение и ссылка на корень сайта.
Напомню что при инициации авторизации вместо «Тела страницы» компонент system.auth.authorize подключится ядром Битрикса только в случае, если пользователь явно не запросил какой-нибудь другой системный компонент из списка:
— ?forgot_password=yes подключит system.auth.forgotpasswd (отправка контрольного слова для смены пароля),
— ?change_password=yes подключит system.auth.changepasswd (смена пароля по контрольному слову),
— ?register=yes подключит system.auth.registration (регистрация),
— ?confirm_registration=yes подключит system.auth.confirmation (подтверждение регистрации).
Такой подход позволяет пользователю восстановить пароль или даже зарегистрировать аккаунт, не уходя с целевой страницы. Вы можете манипулировать доступным набором этих ссылок в своих шаблонах системных компонентов, но запретить их вызов при переходе по этим ссылкам нельзя, и это жирный минус.
Иногда требуется встроить форму авторизации не вместо тела страницы, а внутрь тела страницы, например по адресу /about/index.php посреди контента. И тут возникает нюанс: оказывается системный компонент system.auth.authorize не делает совершенно ничего в отношении непосредственной авторизации, и поэтому сам ничего не знает о результате авторизации. Поэтому если мы подключим компонент без параметров, он будет корректно авторизовывать пользователей, но не будет отображать ошибки авторизации. На самом деле механизм авторизации происходит где-то глубоко в недрах ядра, после чего ядро подключает компонент, передавая в него параметр «AUTH_RESULT» устанавливая значение из $APPLICATION->arAuthResult, что мы вынуждены повторять на своих страницах:
Если же необходимость в авторизации возникает во время выполнения компонента, следует вызвать форму авторизации методом CMain::AuthForm(), что позволит пользователю авторизоваться не только не покидая целевой страницы, но и сохранить все параметры в URL:
Для того чтобы настроить внешний вид системных форм авторизации, регистрации, запроса смены пароля и смены пароля по контрольной строке, вам следует скопировать существующие шаблоны этих компонентов (/bitrix/components/bitrix/system.auth.*/templates/.default) в шаблон сайта (/bitrix/templates/<шаблон_сайта>/components/bitrix/system.auth.*/.default), после чего работать с ними. Вы не можете изменять названия передаваемых полей в этих формах, но можете добавлять любые другие поля, например «телефон» в регистрацию, правда на данном этапе системный компонент не поймет чего вы от него хотите этими новыми полями. Так же вы можете полностью изменить дизайн и регулировать отображение ссылок на соседние системные компоненты. При переработке этих шаблонов обратите внимание на вывод сообщений глобальной функцией ShowMessage, принимающей на вход строку или массив. Можно избавиться от ее использования, но лучше переопределить шаблон компонента system.show_message.
К сожалению сегодня системные компоненты реализованы в процедурном стиле, что не позволяет наследовать собственные компоненты от них, переопределяя и дополняя функциональность, поэтому для решения таких задач приходится использовать события Битрикса. Два наиболее интересных события, генерируемых при регистрации пользователя это: OnBeforeUserRegister и OnAfterUserAdd. В идеале обработчики этих событий регистрируются при установке модуля, удаляются при удалении модуля, например:
Событие OnBeforeUserRegister примечательно тем, что не вызывается при обычном добавлении пользователя, будь то вызов API или ручное добавление в административной части, что позволяет изменить логику именно регистрации пользователя-гостя, накладывая дополнительные ограничения на поля или обрабатывая дополнительные поля, например контроль за цифрами в телефоне, контроль за наличием данных в имени и фамилии.
Задача обработчика события — скорректировать входящий массив данных $arArgs или объявить об ошибке методом CMain::ThrowException(), вернув false вместо массива данных. К сожалению добиться того чтобы интересующие нас поля передавались через $arArgs невозможно, поэтому приходится анализировать $_REQUEST. Листинг можно улучшить, сгруппировав найденные ошибки, но к сожалению даже если обработчик успешно отработает, в дальнейшем в процессе регистрации в ядре могут возникнуть другие ошибки, влиять на которые мы не можем. Пользователь не доволен, получая разные ошибки в процессе регистрации. Программист ищет альтернативы.
Событие OnAfterUserAdd вызывается не только после успешной самостоятельной регистрации пользователя, но и при вызовах API или добавлении пользователя в административной части, но передаваемый в обработчики этого события массив данных ни на что не влияет кроме самих обработчиков, поэтому генерировать исключения здесь практически бесполезно, но зато вместо этого можно выполнить некоторую общую для всех новых пользователей логику, как то: добавить пользователя в лист рассылки, создать для него бюджет в интернет-магазине или присвоить какое-либо пользовательское свойство.
Если набраться смелости и залезть в исходный код системных компонентов (авторизации), то у вас резко пропадет любое желание работы с ними. Как очевидный альтернативный вариант на ум в первую очередь приходит создание собственных компонентов авторизации, регистрации и прочего. Благо API позволяет реализовать все тоже самое. Однако в этой альтернативе я вижу следующие жирные минусы:
— затраты на повторную реализацию логики (в одном лишь компоненте авторизации: проверочная капча, безопасная авторизация по ключу и авторизация через внешние сервисы),
— отказ от обновлений рассматриваемых системных компонентов, что особенно актуально с постепенным переходом ядра на D7,
— невозможность подключить собственный компонент авторизации на целевую страницу пользователя в случае когда ему недостаточно прав для просмотра страницы, вместо этого приходится реализовывать свою логику доступности тех или иных страниц и разделов.
Другой альтернативой можно считать компонент bitrix:main.register, обладающий широким спектром параметров. Однако для того чтобы именно этот компонент отображался и отрабатывал на целевой странице пользователя, программисту приходится идти на ухищрение: вставлять вызов этого компонента в шаблон системного компонента регистрации (bitrix:system.auth.register), что влечет за собой следующие недостатки:
— один и тот же набор параметров компонента хранится как минимум в двух местах: в публичной части на странице авторизации и в шаблоне сайта,
— при вызове bitrix:main.register через шаблон компонента bitrix:system.auth.register код последнего отрабатывает впустую.
Не секрет что платформа Битрикс далека от идеала. Каждый раз когда я встречаю подобные вещи, искажающие мой покерфейс, я сперва называю из странными, затем пытаюсь разобраться и понять почему же было сделано именно так и в большинстве случаев нахожу приемлемый ответ. Но в этом случае ответа я не нашел, поэтому решил записать все соображения по поводу. Надеюсь, кому-нибудь пригодится.
Принцип работы
По умолчанию для авторизации и регистрации используются старые компоненты 1.0, использование которых нежелательно. Исправить это можно, отметив галку «Использовать Компоненты 2.0 для авторизации и регистрации» в настройках главного модуля или однократно выполнив код:
COption::SetOptionString('main', 'auth_comp2', 'Y');
Ознакомившись с описанием порядка выполнения страницы мы видим, что на шаге 1.13 (еще до вывода header.php из шаблона сайта) идет проверка прав доступа уровня 1 и если она не пройдена, то выводится форма авторизации. Разберем это подробнее.
Под вызовом формы регистрации подразумеваются следующие действия:
— порядок выполнения страницы продолжает выполнятся без изменений вплоть до пункта 3 «Тело страницы», включая выполнение header.php,
— вместо «Тела страницы» в зависимости от переданных в $_REQUEST переменных подключается один из системных компонентов system.auth.* (конкретно system.auth.authorize если $_REQUEST пустой), в который передаются особые параметры (об этом ниже),
— порядок выполнения страницы продолжает выполнятся без изменений, включая выполнение footer.php.
Это позволяет реализовать собственный дизайн (шаблон) для компонента system.auth.authorize, органично вписывающийся в шаблон сайта между шапкой и подвалом.
Проверка прав доступа «уровня 1» инициирует вызов формы авторизации в следующих случаях:
— если глобальная константа NEED_AUTH определена и равна true,
— если у пользователя (в том числе незарегистрированного) недостаточно прав для чтения запрошенного им файла.
Права на чтение файлов и разделов для разных групп пользователей формируются при редактировании оных административной панелью в служебных файлах .access.php. Каждый файл описывает уровни доступа к файлам и каталогам, расположенным на одном с ним уровне. Возможно создание файла .access.php для каждого каталога в публичной части сайта, каждый последующий такой файл дополняет правила доступа по цепочке каталогов. Пример такого файла в корне сайта:
<?
$PERM["/"]["*"]="D"; // запрещает всем группам пользователей доступ ко всем файлам и каталогам
$PERM["/"]["AU"]="R"; // разрешает авторизованным группам пользователей доступ ко всем файлам и каталогам
$PERM["about"]["*"]="R"; // разрешает всем группам пользователей доступ к каталогу about
$PERM["login"]["*"]="R"; // разрешает всем группам пользователей доступ к каталогу login
$PERM["index.php"]["*"]="R"; // разрешает всем группам пользователей доступ к файлу index.php
?>
Такое распределение прав позволяет буквально «на месте» запрашивать логин и пароль пользователя в случаях когда он переходит без активной авторизации в каталог, функционал которого требует авторизацию. Такое часто случается, если пользователь держит в избранном ссылку не на корень сайта, а на конкретный раздел, с которым больше всего работает. В результате пользователь вводит логин и пароль не уходя с целевой страницы, те отправляются на ее URL, вместо страницы отрабатывает системный компонент обрабатывая полученные данные либо сыпет ошибку либо авторизует и перезагружает страницу. Пользователь доволен, ему не надо совершать лишний клик для перехода в нужный раздел. Программист доволен, ему не нужно думать о редиректах на /auth/ и $backurl.
Использовать же глобальную константу NEED_AUTH рекомендую только в специфических случаях, одним из которых является непосредственно страница авторизации в стандартной поставке демо-сайта по адресу /auth/index.php:
<?
define("NEED_AUTH", true);
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
IncludeModuleLangFile($_SERVER["DOCUMENT_ROOT"]."/bitrix/modules/intranet/public/about/auth/index.php");
// ...
$APPLICATION->SetTitle(GetMessage("AUTH_TITLE"));
?>
<p><?=GetMessage("AUTH_SUCCESS")?></p>
<p><a href="<?=SITE_DIR?>"><?=GetMessage("AUTH_BACK")?></a></p>
<?require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php");?>
Этот скрипт передает управление ядру Битрикса, определив перед этим константу NEED_AUTH. Битрикс, в случае если пользователь не авторизован, подключает шапку + системный компонент (авторизации) + подвал и умирает, не позволяя скрипту выполняться дальше второй строчки. В случае же если пользователь авторизован — ему выводится приветственное сообщение и ссылка на корень сайта.
Прочие системные компоненты
Напомню что при инициации авторизации вместо «Тела страницы» компонент system.auth.authorize подключится ядром Битрикса только в случае, если пользователь явно не запросил какой-нибудь другой системный компонент из списка:
— ?forgot_password=yes подключит system.auth.forgotpasswd (отправка контрольного слова для смены пароля),
— ?change_password=yes подключит system.auth.changepasswd (смена пароля по контрольному слову),
— ?register=yes подключит system.auth.registration (регистрация),
— ?confirm_registration=yes подключит system.auth.confirmation (подтверждение регистрации).
Такой подход позволяет пользователю восстановить пароль или даже зарегистрировать аккаунт, не уходя с целевой страницы. Вы можете манипулировать доступным набором этих ссылок в своих шаблонах системных компонентов, но запретить их вызов при переходе по этим ссылкам нельзя, и это жирный минус.
Использование компонента авторизации внутри тела страницы
Иногда требуется встроить форму авторизации не вместо тела страницы, а внутрь тела страницы, например по адресу /about/index.php посреди контента. И тут возникает нюанс: оказывается системный компонент system.auth.authorize не делает совершенно ничего в отношении непосредственной авторизации, и поэтому сам ничего не знает о результате авторизации. Поэтому если мы подключим компонент без параметров, он будет корректно авторизовывать пользователей, но не будет отображать ошибки авторизации. На самом деле механизм авторизации происходит где-то глубоко в недрах ядра, после чего ядро подключает компонент, передавая в него параметр «AUTH_RESULT» устанавливая значение из $APPLICATION->arAuthResult, что мы вынуждены повторять на своих страницах:
<?
require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/header.php");
$APPLICATION->SetTitle("О сайте");
?>
...
<? if (!$USER->IsAuthorized()): ?>
<? $APPLICATION->IncludeComponent('bitrix:system.auth.authorize', '', array('AUTH_RESULT' => $APPLICATION->arAuthResult)); ?>
<? endif; ?>
...
<? require($_SERVER["DOCUMENT_ROOT"] . "/bitrix/footer.php"); ?>
Если же необходимость в авторизации возникает во время выполнения компонента, следует вызвать форму авторизации методом CMain::AuthForm(), что позволит пользователю авторизоваться не только не покидая целевой страницы, но и сохранить все параметры в URL:
$APPLICATION->AuthForm(array(
"MESSAGE" => "Для работы с ... требуется авторизация.",
"TYPE" => "OK",
));
Создание собственных шаблонов
Для того чтобы настроить внешний вид системных форм авторизации, регистрации, запроса смены пароля и смены пароля по контрольной строке, вам следует скопировать существующие шаблоны этих компонентов (/bitrix/components/bitrix/system.auth.*/templates/.default) в шаблон сайта (/bitrix/templates/<шаблон_сайта>/components/bitrix/system.auth.*/.default), после чего работать с ними. Вы не можете изменять названия передаваемых полей в этих формах, но можете добавлять любые другие поля, например «телефон» в регистрацию, правда на данном этапе системный компонент не поймет чего вы от него хотите этими новыми полями. Так же вы можете полностью изменить дизайн и регулировать отображение ссылок на соседние системные компоненты. При переработке этих шаблонов обратите внимание на вывод сообщений глобальной функцией ShowMessage, принимающей на вход строку или массив. Можно избавиться от ее использования, но лучше переопределить шаблон компонента system.show_message.
Расширение функционала регистрации
К сожалению сегодня системные компоненты реализованы в процедурном стиле, что не позволяет наследовать собственные компоненты от них, переопределяя и дополняя функциональность, поэтому для решения таких задач приходится использовать события Битрикса. Два наиболее интересных события, генерируемых при регистрации пользователя это: OnBeforeUserRegister и OnAfterUserAdd. В идеале обработчики этих событий регистрируются при установке модуля, удаляются при удалении модуля, например:
// при установке модуля:
RegisterModuleDependences("main", "OnAfterUserAdd", $this->MODULE_ID, "CWBroker", "OnAfterUserAdd");
RegisterModuleDependences("main", "OnBeforeUserRegister", $this->MODULE_ID, "CWBroker", "OnBeforeUserRegister");
// при удалении модуля:
UnRegisterModuleDependences("main", "OnAfterUserAdd", $this->MODULE_ID, "CWBroker", "OnAfterUserAdd");
UnRegisterModuleDependences("main", "OnBeforeUserRegister", $this->MODULE_ID, "CWBroker", "OnBeforeUserRegister");
Событие OnBeforeUserRegister примечательно тем, что не вызывается при обычном добавлении пользователя, будь то вызов API или ручное добавление в административной части, что позволяет изменить логику именно регистрации пользователя-гостя, накладывая дополнительные ограничения на поля или обрабатывая дополнительные поля, например контроль за цифрами в телефоне, контроль за наличием данных в имени и фамилии.
class CWBroker {
// ...
public static function OnBeforeUserRegister(&$arArgs) {
global $APPLICATION;
$_REQUEST['USER_PERSONAL_PHONE'] = preg_replace('#[^0-9]#', '', $_REQUEST['USER_PERSONAL_PHONE']);
$arArgs['LOGIN'] = $arArgs['EMAIL'];
$arArgs['CONFIRM_PASSWORD'] = $arArgs['PASSWORD'];
$arArgs['PERSONAL_PHONE'] = $_REQUEST['USER_PERSONAL_PHONE'];
$arArgs['PERSONAL_STATE'] = $_REQUEST['USER_PERSONAL_STATE'];
if (empty($arArgs['NAME'])) {
$APPLICATION->ThrowException('Не указано имя');
return false;
}
if (empty($arArgs['LAST_NAME'])) {
$APPLICATION->ThrowException('Не указана фамилия');
return false;
}
if (empty($arArgs['PERSONAL_PHONE'])) {
$APPLICATION->ThrowException('Не указан мобильный телефон');
return false;
}
if (empty($arArgs['PERSONAL_STATE'])) {
$APPLICATION->ThrowException('Не указан регион');
return false;
}
if ($_REQUEST['USER_AGREE_TERMS'] <> 'Y' || $_REQUEST['USER_AGREE_PHONE'] <> 'Y' || $_REQUEST['USER_AGREE_EMAIL'] <> 'Y') {
$APPLICATION->ThrowException('Необходимо принять все условия');
return false;
}
return $arArgs;
}
// ...
}
Задача обработчика события — скорректировать входящий массив данных $arArgs или объявить об ошибке методом CMain::ThrowException(), вернув false вместо массива данных. К сожалению добиться того чтобы интересующие нас поля передавались через $arArgs невозможно, поэтому приходится анализировать $_REQUEST. Листинг можно улучшить, сгруппировав найденные ошибки, но к сожалению даже если обработчик успешно отработает, в дальнейшем в процессе регистрации в ядре могут возникнуть другие ошибки, влиять на которые мы не можем. Пользователь не доволен, получая разные ошибки в процессе регистрации. Программист ищет альтернативы.
Событие OnAfterUserAdd вызывается не только после успешной самостоятельной регистрации пользователя, но и при вызовах API или добавлении пользователя в административной части, но передаваемый в обработчики этого события массив данных ни на что не влияет кроме самих обработчиков, поэтому генерировать исключения здесь практически бесполезно, но зато вместо этого можно выполнить некоторую общую для всех новых пользователей логику, как то: добавить пользователя в лист рассылки, создать для него бюджет в интернет-магазине или присвоить какое-либо пользовательское свойство.
class CWBroker {
// ...
public static function OnAfterUserAdd(&$arFields) {
if (CModule::IncludeModule('inquiries')) {
$CUser = new CUser();
$CUser->Update($arFields['ID'], array('UF_TARIFF' => 1));
}
if (CModule::IncludeModule('sale')) {
$sale_account = CSaleUserAccount::GetByUserID($arFields['ID'], 'RUB');
if (empty($sale_account)) {
CSaleUserAccount::Add(array(
'USER_ID' => $arFields['ID'],
'CURRENT_BUDGET' => 0,
'CURRENCY' => 'RUB',
));
}
}
if (CModule::IncludeModule('subscribe')) {
$CDBResult = CRubric::GetList(array("SORT" => "ASC"), array("ACTIVE" => "Y"));
$rubrics = array();
while ($rubric = $CDBResult->Fetch()) {
$rubrics[] = $rubric['ID'];
}
$CSubscription = new CSubscription();
$CSubscription->Add(array(
'USER_ID' => $arFields['ID'],
'EMAIL' => $arFields['EMAIL'],
'FORMAT' => 'html',
'CONFIRMED' => 'Y',
'SEND_CONFIRM' => 'N',
'RUB_ID' => $rubrics,
));
}
}
// ...
}
Альтернативы
Если набраться смелости и залезть в исходный код системных компонентов (авторизации), то у вас резко пропадет любое желание работы с ними. Как очевидный альтернативный вариант на ум в первую очередь приходит создание собственных компонентов авторизации, регистрации и прочего. Благо API позволяет реализовать все тоже самое. Однако в этой альтернативе я вижу следующие жирные минусы:
— затраты на повторную реализацию логики (в одном лишь компоненте авторизации: проверочная капча, безопасная авторизация по ключу и авторизация через внешние сервисы),
— отказ от обновлений рассматриваемых системных компонентов, что особенно актуально с постепенным переходом ядра на D7,
— невозможность подключить собственный компонент авторизации на целевую страницу пользователя в случае когда ему недостаточно прав для просмотра страницы, вместо этого приходится реализовывать свою логику доступности тех или иных страниц и разделов.
Другой альтернативой можно считать компонент bitrix:main.register, обладающий широким спектром параметров. Однако для того чтобы именно этот компонент отображался и отрабатывал на целевой странице пользователя, программисту приходится идти на ухищрение: вставлять вызов этого компонента в шаблон системного компонента регистрации (bitrix:system.auth.register), что влечет за собой следующие недостатки:
— один и тот же набор параметров компонента хранится как минимум в двух местах: в публичной части на странице авторизации и в шаблоне сайта,
— при вызове bitrix:main.register через шаблон компонента bitrix:system.auth.register код последнего отрабатывает впустую.
В заключение
Не секрет что платформа Битрикс далека от идеала. Каждый раз когда я встречаю подобные вещи, искажающие мой покерфейс, я сперва называю из странными, затем пытаюсь разобраться и понять почему же было сделано именно так и в большинстве случаев нахожу приемлемый ответ. Но в этом случае ответа я не нашел, поэтому решил записать все соображения по поводу. Надеюсь, кому-нибудь пригодится.