Блог разработчика 1С-Битрикс

Единое поле «Логин / Email / Телефон» в 1С-Битрикс: полноценное руководство с рабочими примерами

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

Единое поле «Логин / Email / Телефон» в 1С-Битрикс

Что будем делать

  • Сделаем кастомную форму авторизации с одним полем.
  • Реализуем корректный бэкенд: поиск пользователя по email/телефону/логину, проверка пароля, редирект.
  • Нормализуем телефоны при регистрации/изменении профиля.
  • Покажем альтернативу без кастомной формы — через обработчик события OnBeforeUserLogin.
  • Учтём безопасность (CSRF, «remember me», ограничение редиректов), UX-мелочи и частые грабли.

Подготовка

  1. Телефоны храним в едином формате: +79991234567 (E.164).
    Для РФ добавляем код 7, для номера на «8» меняем на «7».
  2. Рекомендуется использовать поля PERSONAL_MOBILE или PERSONAL_PHONE. Если в проекте есть кастомное UF-поле (например, UF_PHONE), учтём его в поиске.
  3. Включите отображение ошибок в 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. Чек-лист тестирования

  1. Email: user@example.com + верный/неверный пароль.
  2. Телефон: +7 (999) 123-45-67, 8 (999) 123-45-67, 9991234567 — все должны находить одну запись.
  3. Логин: user123.
  4. Неактивный пользователь: ACTIVE='N' — вход не должен проходить.
  5. Дубликаты: два пользователя с одинаковым телефоном — регистрация/сохранение должно «заругаться».
  6. Редирект: передать ?backurl=/personal/orders/ — должен попасть туда, внешние URL — игнорируются.
  7. CSRF: запрос без sessid — должен отвергаться.

Частые вопросы

Можно ли использовать \Bitrix\Main\UserTable (D7) вместо CUser::GetList?
Да. Но CUser::GetList остаётся рабочим и простым для задач авторизации. Если используете D7, соблюдайте точное сравнение и лимиты.
Что если телефон не российский?
Функция normalizePhone() оставляет все цифры и ставит + спереди. Для международного номера пользователь должен ввести +<код страны>. Логику по странам можно расширить под проект.
Нужно ли менять стандартный компонент bitrix:system.auth.form?
Не обязательно. Используйте вариант с OnBeforeUserLogin — он «переведёт» любой ввод в правильный LOGIN.
Почему не ищем по подстроке?
Поиск по подстроке («содержит») даёт ложные сработки и мешает индексации. Для авторизации нужен строгий матч нормализованного значения.

Итог

Вы получили два полноценных пути:

  1. Кастомная форма с единым полем и правильной серверной логикой — гибко и прозрачно.
  2. Автоподмена в обработчике OnBeforeUserLogin — минимум правок, работает «повсеместно».

Оба варианта используют точные фильтры, нормализацию телефонов, защиту от CSRF и безопасные редиректы. С такими кирпичами единое поле «Логин / Email / Телефон» в 1С-Битрикс будет работать так, как этого ждут пользователи и поисковики — быстро, предсказуемо и безопасно.

Теги:  авторизация, единое поле, login, email, телефон, CUser::GetList, OnBeforeUserLogin, безопасность, normalizePhone, PHP, UX, вход, форма входа, регистрация, нормализация номера, CSRF, open redirect


Стоимость услуг по разработке и сопровождению сайтов на 1C-Битрикс

Техническая поддержка

выполняется с сайтами на основе любых CMS

от 5 000 рублей
Оптимизация производительности действующих интернет-проектов, наполнение и сопровождение, полная техническая поддержка и продвижение в поисковых сетях.

* стоимость зависит от объема и сложности выполняемых работ, используемой CMS.

Разработка корпоративного сайта

от 7 дней

от 40 000 рублей

Разработка сайта без системы оплаты заказов через корзину

* стоимость зависит от наличия верстки, использования готового решения и т.д.

Разработка интернет-магазина с готовой версткой

от 4 недель

от 90 000 рублей

* указана минимальная стоимость. Стоимость выбранной лицензии «1С-Битрикс» оплачивается отдельно.