Единое поле авторизации — это удобный UX-паттерн: пользователь вводит одну строку (логин, email или телефон), а система сама определяет тип и авторизует. Ниже — цельное, «боевое» решение для Битрикс: Управление сайтом / Битрикс24 коробка с акцентом на правильные фильтры, безопасность и обслуживание.

Что будем делать
- Сделаем кастомную форму авторизации с одним полем.
- Реализуем корректный бэкенд: поиск пользователя по email/телефону/логину, проверка пароля, редирект.
- Нормализуем телефоны при регистрации/изменении профиля.
- Покажем альтернативу без кастомной формы — через обработчик события
OnBeforeUserLogin
. - Учтём безопасность (CSRF, «remember me», ограничение редиректов), UX-мелочи и частые грабли.
Подготовка
- Телефоны храним в едином формате:
+79991234567
(E.164).
Для РФ добавляем код7
, для номера на «8» меняем на «7». - Рекомендуется использовать поля
PERSONAL_MOBILE
илиPERSONAL_PHONE
. Если в проекте есть кастомное UF-поле (например,UF_PHONE
), учтём его в поиске. - Включите отображение ошибок в dev-среде для отладки (на проде — нет).
Часть 1. Кастомная форма авторизации (рекомендуется)
Создадим /auth/index.php
и полностью возьмём на себя обработку:
<?php
require($_SERVER["DOCUMENT_ROOT"]."/bitrix/header.php");
/**
* ЕДИНОЕ ПОЛЕ АВТОРИЗАЦИИ ДЛЯ БИТРИКС
*/
use Bitrix\Main\Loader;
use Bitrix\Main\Context;
use Bitrix\Main\Application;
use Bitrix\Main\Web\Uri;
global $USER, $APPLICATION;
Loader::includeModule('main');
// -------------------------------
// Вспомогательные функции
// -------------------------------
/**
* Нормализует телефон в формат +7XXXXXXXXXX (для РФ) или +<digits>.
* Оставляет только цифры и плюс в начале. Возвращает строку или null, если цифр < 10.
*/
function normalizePhone(string $raw, string $defaultCountry = '7'): ?string
{
$digits = preg_replace('/\D+/', '', $raw ?? '');
if ($digits === '' || strlen($digits) < 10) {
return null;
}
// РФ-специфика: 10 цифр -> добавляем 7; 11 на 8 -> меняем на 7
if (strlen($digits) === 10) {
$digits = $defaultCountry . $digits; // 7XXXXXXXXXX
} elseif (strlen($digits) === 11 && $digits[0] === '8') {
$digits = $defaultCountry . substr($digits, 1);
}
return '+' . $digits;
}
/** Находит LOGIN по email (точное совпадение, только активные) */
function findLoginByEmail(string $email): ?string
{
$by = "id"; $order = "asc";
$filter = [
"=EMAIL" => $email,
"ACTIVE" => "Y",
];
$params = ["FIELDS" => ["ID","LOGIN"], "NAV_PARAMS" => ["nTopCount" => 1]];
$rs = CUser::GetList($by, $order, $filter, $params);
if ($u = $rs->Fetch()) {
return $u["LOGIN"];
}
return null;
}
/** Находит LOGIN по телефону (точное совпадение, учёт нескольких полей) */
function findLoginByPhone(string $normalized): ?string
{
$by = "id"; $order = "asc";
$params = ["FIELDS" => ["ID","LOGIN"], "NAV_PARAMS" => ["nTopCount" => 1]];
// 1) PERSONAL_MOBILE
$rs = CUser::GetList($by, $order, ["=PERSONAL_MOBILE" => $normalized, "ACTIVE" => "Y"], $params);
if ($u = $rs->Fetch()) {
return $u["LOGIN"];
}
// 2) PERSONAL_PHONE
$rs = CUser::GetList($by, $order, ["=PERSONAL_PHONE" => $normalized, "ACTIVE" => "Y"], $params);
if ($u = $rs->Fetch()) {
return $u["LOGIN"];
}
// 3) Кастомное UF-поле (если используется)
$rs = CUser::GetList($by, $order, ["=UF_PHONE" => $normalized, "ACTIVE" => "Y"], $params);
if ($u = $rs->Fetch()) {
return $u["LOGIN"];
}
return null;
}
/** Проверка, что редирект внутренний (безопасность против open redirect) */
function safeBackUrl(string $url, string $fallback = "/"): string
{
$uri = new Uri($url);
// Разрешаем только относительные пути и тот же хост
$host = Context::getCurrent()->getRequest()->getHttpHost();
if ($uri->getHost() && $uri->getHost() !== $host) {
return $fallback;
}
$path = $uri->getPath() ?: '/';
// Не позволяем уходить в /bitrix/admin/ без прав и т.п. — при необходимости расширьте правила
return $path . ($uri->getQuery() ? '?' . $uri->getQuery() : '');
}
// -------------------------------
// Обработка формы
// -------------------------------
$request = Context::getCurrent()->getRequest();
$errors = [];
$loginInput = '';
$remember = $request->getPost("USER_REMEMBER") === "Y";
$backurl = $request->get("backurl"); // может прийти как GET|POST
$redirectTo = safeBackUrl((string)$backurl, "/");
if ($request->isPost() && $request->get("auth") === "Y") {
if (!check_bitrix_sessid()) {
$errors[] = "Сессия истекла. Обновите страницу и повторите попытку.";
} else {
$loginInput = trim((string)$request->getPost("USER_LOGIN"));
$password = (string)$request->getPost("USER_PASSWORD");
if ($loginInput === '' || $password === '') {
$errors[] = "Заполните логин/email/телефон и пароль.";
} else {
$realLogin = null;
// 1) Email?
if (filter_var($loginInput, FILTER_VALIDATE_EMAIL)) {
$realLogin = findLoginByEmail($loginInput);
} else {
// 2) Телефон?
$normalized = normalizePhone($loginInput);
if ($normalized) {
$realLogin = findLoginByPhone($normalized);
}
// 3) Иначе считаем, что логин
if (!$realLogin) {
$by = "id"; $order = "asc";
$rs = CUser::GetList($by, $order, ["=LOGIN" => $loginInput, "ACTIVE" => "Y"], ["FIELDS" => ["ID","LOGIN"], "NAV_PARAMS" => ["nTopCount" => 1]]);
if ($u = $rs->Fetch()) {
$realLogin = $u["LOGIN"];
}
}
}
if ($realLogin) {
// ВАЖНО: CUser::Login возвращает массив с результатом, а не boolean
$authResult = $USER->Login($realLogin, $password, $remember ? "Y" : "N");
if (is_array($authResult) && $authResult["TYPE"] === "OK") {
LocalRedirect($redirectTo);
die();
} else {
// Сообщение от ядра может содержать HTML — выводим безопасно
$errors[] = "Неверный логин/email/телефон или пароль.";
}
} else {
$errors[] = "Пользователь не найден.";
}
}
}
}
?>
<h1>Вход в личный кабинет</h1>
<?php if (!empty($errors)): ?>
<div style="color:#b00020; margin:0 0 16px;">
<?php foreach ($errors as $e): ?>
<div><?=htmlspecialcharsbx($e)?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<form method="post" action="<?=htmlspecialcharsbx($APPLICATION->GetCurPageParam("", ["logout"]))?>">
<?=bitrix_sessid_post()?>
<input type="hidden" name="auth" value="Y">
<?php if ($redirectTo && $redirectTo !== "/"): ?>
<input type="hidden" name="backurl" value="<?=htmlspecialcharsbx($redirectTo)?>">
<?php endif; ?>
<label style="display:block; margin-bottom:8px;">
Логин, email или телефон
<input
type="text"
name="USER_LOGIN"
value="<?=htmlspecialcharsbx($loginInput)?>"
required
autocomplete="username"
inputmode="email"
style="display:block; width:100%; max-width:360px; padding:8px; margin-top:4px;"
placeholder="user123 · user@example.com · +7 999 123-45-67"
>
</label>
<label style="display:block; margin-bottom:8px;">
Пароль
<input
type="password"
name="USER_PASSWORD"
required
autocomplete="current-password"
style="display:block; width:100%; max-width:360px; padding:8px; margin-top:4px;"
placeholder="Ваш пароль"
>
</label>
<label style="display:flex; align-items:center; gap:8px; margin:8px 0 16px;">
<input type="checkbox" name="USER_REMEMBER" value="Y" <?php if ($remember) echo 'checked'; ?>>
<span>Запомнить меня</span>
</label>
<button type="submit" style="padding:10px 16px;">Войти</button>
<div style="margin-top:8px; font-size:12px; color:#666;">
Подсказка: введите логин, email или телефон в любом формате — мы разберёмся.
</div>
</form>
<?php require($_SERVER["DOCUMENT_ROOT"]."/bitrix/footer.php"); ?>
Часть 2. Нормализация телефонов при регистрации/обновлении
Чтобы поиск по телефону работал «в один клик», важно сохранять телефоны уже нормализованными. Добавим обработчики в /bitrix/php_interface/init.php
.
<?php
use Bitrix\Main\EventManager;
if (!function_exists('normalizePhone')) {
function normalizePhone(string $raw, string $defaultCountry = '7'): ?string
{
$digits = preg_replace('/\D+/', '', $raw ?? '');
if ($digits === '' || strlen($digits) < 10) {
return null;
}
if (strlen($digits) === 10) {
$digits = $defaultCountry . $digits;
} elseif (strlen($digits) === 11 && $digits[0] === '8') {
$digits = $defaultCountry . substr($digits, 1);
}
return '+' . $digits;
}
}
/** Проверяем уникальность телефона среди активных пользователей */
function phoneExists(string $normalized, ?int $excludeUserId = null): bool
{
$by = "id"; $order = "asc";
$filterCommon = ["ACTIVE" => "Y"];
$params = ["FIELDS" => ["ID"], "NAV_PARAMS" => ["nTopCount" => 1]];
foreach (["=PERSONAL_MOBILE", "=PERSONAL_PHONE", "=UF_PHONE"] as $key) {
$filter = $filterCommon + [$key => $normalized];
$rs = CUser::GetList($by, $order, $filter, $params);
if ($u = $rs->Fetch()) {
if ($excludeUserId === null || (int)$u["ID"] !== (int)$excludeUserId) {
return true;
}
}
}
return false;
}
EventManager::getInstance()->addEventHandler('main', 'OnBeforeUserRegister', function (&$fields) {
// Нормализуем и раскладываем по полям
if (!empty($fields['PERSONAL_MOBILE'])) {
$n = normalizePhone($fields['PERSONAL_MOBILE']);
if ($n) { $fields['PERSONAL_MOBILE'] = $n; }
}
if (!empty($fields['PERSONAL_PHONE'])) {
$n = normalizePhone($fields['PERSONAL_PHONE']);
if ($n) { $fields['PERSONAL_PHONE'] = $n; }
}
if (!empty($fields['UF_PHONE'])) {
$n = normalizePhone($fields['UF_PHONE']);
if ($n) { $fields['UF_PHONE'] = $n; }
}
// Проверка уникальности (на пример — по PERSONAL_MOBILE)
$phone = $fields['PERSONAL_MOBILE'] ?? $fields['PERSONAL_PHONE'] ?? $fields['UF_PHONE'] ?? null;
if ($phone && phoneExists($phone, null)) {
global $APPLICATION;
$APPLICATION->throwException("Пользователь с таким телефоном уже существует.");
return false;
}
return true;
});
EventManager::getInstance()->addEventHandler('main', 'OnBeforeUserUpdate', function (&$fields) {
$userId = (int)$fields['ID'];
foreach (['PERSONAL_MOBILE','PERSONAL_PHONE','UF_PHONE'] as $f) {
if (array_key_exists($f, $fields) && $fields[$f] !== null) {
$n = normalizePhone((string)$fields[$f]);
if ($n) {
$fields[$f] = $n;
}
}
}
$phone = $fields['PERSONAL_MOBILE'] ?? $fields['PERSONAL_PHONE'] ?? $fields['UF_PHONE'] ?? null;
if ($phone && phoneExists($phone, $userId)) {
global $APPLICATION;
$APPLICATION->throwException("Пользователь с таким телефоном уже существует.");
return false;
}
return true;
});
Часть 3. Альтернатива без кастомной формы: OnBeforeUserLogin
Если вы используете стандартный компонент bitrix:system.auth.form
(или логин идёт через REST/мобильное приложение), можно перехватить вход и подменить LOGIN
на лету. Это сработает для любого способа авторизации.
Добавьте в init.php
:
<?php
use Bitrix\Main\EventManager;
if (!function_exists('normalizePhone')) {
function normalizePhone(string $raw, string $defaultCountry = '7'): ?string
{
$digits = preg_replace('/\D+/', '', $raw ?? '');
if ($digits === '' || strlen($digits) < 10) {
return null;
}
if (strlen($digits) === 10) {
$digits = $defaultCountry . $digits;
} elseif (strlen($digits) === 11 && $digits[0] === '8') {
$digits = $defaultCountry . substr($digits, 1);
}
return '+' . $digits;
}
}
function __findLoginByEmailOrPhone(string $input): ?string
{
// Email
if (filter_var($input, FILTER_VALIDATE_EMAIL)) {
$by="id"; $order="asc";
$rs = CUser::GetList($by, $order, ["=EMAIL"=>$input, "ACTIVE"=>"Y"], ["FIELDS"=>["ID","LOGIN"], "NAV_PARAMS"=>["nTopCount"=>1]]);
if ($u = $rs->Fetch()) return $u["LOGIN"];
}
// Телефон
if ($n = normalizePhone($input)) {
foreach (["=PERSONAL_MOBILE","=PERSONAL_PHONE","=UF_PHONE"] as $k) {
$by="id"; $order="asc";
$rs = CUser::GetList($by, $order, [$k=>$n, "ACTIVE"=>"Y"], ["FIELDS"=>["ID","LOGIN"], "NAV_PARAMS"=>["nTopCount"=>1]]);
if ($u = $rs->Fetch()) return $u["LOGIN"];
}
}
// Иначе — возможно, это уже логин
$by="id"; $order="asc";
$rs = CUser::GetList($by, $order, ["=LOGIN"=>$input, "ACTIVE"=>"Y"], ["FIELDS"=>["ID","LOGIN"], "NAV_PARAMS"=>["nTopCount"=>1]]);
if ($u = $rs->Fetch()) return $u["LOGIN"];
return null;
}
EventManager::getInstance()->addEventHandler('main', 'OnBeforeUserLogin', function (&$arFields) {
if (empty($arFields['LOGIN'])) {
return;
}
$found = __findLoginByEmailOrPhone((string)$arFields['LOGIN']);
if ($found) {
$arFields['LOGIN'] = $found; // подменяем логин — дальше Битрикс сам проверит пароль
}
});
Минусы: сложнее дебажить UX-ошибки на фронте, и «забыли пароль»/капча могут требовать отдельной доработки.
Часть 4. Пример регистрации с записью нормализованного телефона
Если вы делаете собственную форму регистрации:
<?php
use Bitrix\Main\Loader;
Loader::includeModule('main');
global $USER;
$cUser = new CUser();
$login = trim($_POST['LOGIN'] ?? '');
$email = trim($_POST['EMAIL'] ?? '');
$password = (string)($_POST['PASSWORD'] ?? '');
$phoneRaw = (string)($_POST['PHONE'] ?? '');
$phone = normalizePhone($phoneRaw); // та же функция
$fields = [
"LOGIN" => $login,
"EMAIL" => $email,
"PASSWORD" => $password,
"CONFIRM_PASSWORD" => $password,
"ACTIVE" => "Y",
"PERSONAL_MOBILE" => $phone ?: null, // либо PERSONAL_PHONE / UF_PHONE
];
$userId = $cUser->Add($fields);
if ((int)$userId > 0) {
// Можно авторизовать сразу:
$USER->Authorize((int)$userId);
LocalRedirect("/personal/");
} else {
echo "Ошибка: ".htmlspecialcharsbx($cUser->LAST_ERROR);
}
Часть 5. Улучшения и безопасность
- CAPTCHA / лимиты попыток. Для публичных проектов используйте встроенную капчу компонента или поставьте rate-limit на POST по IP/пользователю.
- «Запомнить меня». Мы поддержали чекбокс
USER_REMEMBER
→ третий аргумент$USER->Login
. - Сообщения об ошибках. Специально показываем обобщённое «Неверный логин/email/телефон или пароль», чтобы не «подсказывать» злоумышленникам, что именно не так.
- Редиректы. Принудительно оставляем редиректы внутренними (см.
safeBackUrl
), чтобы не было open redirect. - Уникальность телефона. Мы явно проверяем дубликаты в хендлерах регистрации/обновления. При высокой нагрузке можно добавить прикладную «блокировку»/повторную проверку.
- Производительность. Везде используем
nTopCount => 1
и точные фильтры=FIELD
для индексов. На больших базах можно кэшировать маппинг «телефон → ID» в мемкеше/опкеше.
Часть 6. Фронтенд-штрихи (необязательно, но приятно)
Небольшая подсветка ввода и маска телефона улучшат UX. Простейший вариант без сторонних библиотек:
<input
type="text"
name="USER_LOGIN"
required
placeholder="user@example.com или +7 999 123-45-67"
inputmode="email"
pattern=".{3,}"
title="Введите логин, email или телефон"
/>
<script>
(function () {
const fld = document.querySelector('input[name="USER_LOGIN"]');
if (!fld) return;
fld.addEventListener('input', function () {
const v = fld.value.trim();
// Если начинается с цифры/+, подскажем режим телефона
if (/^\+|\d/.test(v)) {
fld.setAttribute('inputmode', 'tel');
} else {
fld.setAttribute('inputmode', 'email');
}
});
})();
</script>
Часть 7. Чек-лист тестирования
- Email:
user@example.com
+ верный/неверный пароль. - Телефон:
+7 (999) 123-45-67
,8 (999) 123-45-67
,9991234567
— все должны находить одну запись. - Логин:
user123
. - Неактивный пользователь:
ACTIVE='N'
— вход не должен проходить. - Дубликаты: два пользователя с одинаковым телефоном — регистрация/сохранение должно «заругаться».
- Редирект: передать
?backurl=/personal/orders/
— должен попасть туда, внешние URL — игнорируются. - CSRF: запрос без
sessid
— должен отвергаться.
Частые вопросы
- Можно ли использовать
\Bitrix\Main\UserTable
(D7) вместоCUser::GetList
? - Да. Но
CUser::GetList
остаётся рабочим и простым для задач авторизации. Если используете D7, соблюдайте точное сравнение и лимиты. - Что если телефон не российский?
- Функция
normalizePhone()
оставляет все цифры и ставит+
спереди. Для международного номера пользователь должен ввести+<код страны>
. Логику по странам можно расширить под проект. - Нужно ли менять стандартный компонент
bitrix:system.auth.form
? - Не обязательно. Используйте вариант с
OnBeforeUserLogin
— он «переведёт» любой ввод в правильный LOGIN. - Почему не ищем по подстроке?
- Поиск по подстроке («содержит») даёт ложные сработки и мешает индексации. Для авторизации нужен строгий матч нормализованного значения.
Итог
Вы получили два полноценных пути:
- Кастомная форма с единым полем и правильной серверной логикой — гибко и прозрачно.
- Автоподмена в обработчике
OnBeforeUserLogin
— минимум правок, работает «повсеместно».
Оба варианта используют точные фильтры, нормализацию телефонов, защиту от CSRF и безопасные редиректы. С такими кирпичами единое поле «Логин / Email / Телефон» в 1С-Битрикс будет работать так, как этого ждут пользователи и поисковики — быстро, предсказуемо и безопасно.