Траектория'26 в Ярославле Конференция о бизнесе для бизнеса, 26 ноября — берём курс на рост Регистрация
Обсудить проект

Два стенда с одним member_id: почему перестают работать приложения Битрикс24

Оценить статью

Клиент пишет: приложение из Маркетплейса перестало работать. Переустановка не помогает, токены на месте, портал жив. Через полчаса разговора выясняется, что неделю назад подняли тестовый стенд — развернули копию боевого портала на другом домене. В этом и дело.

Разбираем, почему копия коробочного Битрикс24 ломает приложения на боевом портале, как это проверить за минуту и как правильно сменить идентификатор на тестовой площадке.

Что такое member_id и откуда он берётся

Приложение Маркетплейса не знает портал по адресу: адрес владелец может сменить в любой момент. Вместо этого у каждого портала есть идентификатор member_id, который приходит приложению при установке и дальше в каждом запросе.

В облаке его выдаёт сам сервис. В коробке он считается так: CRestUtil::getMemberId() спрашивает модуль bitrix24, а если его нет — берёт значение из модуля rest:

// bitrix/modules/rest/lib/oauthservice.php
public static function getMemberId()
{
    if (static::getEngine()->isRegistered())
    {
        return md5(static::getEngine()->getClientId());
    }
    return null;
}

А getClientId() читает опцию модуля:

return Option::get("rest", "service_client_id", false);

То есть member_id коробки — это md5 от идентификатора, который портал получил при регистрации в сервисе авторизации Битрикса. Регистрация происходит один раз: портал отправляет запрос на сервер OAuth, подписывая его своим лицензионным ключом, и получает в ответ пару client_id и client_secret.

Дальше этот идентификатор участвует в трёх местах сразу: он уходит в каждое исходящее событие приложению, добавляется в запросы на обновление токенов и передаётся в Маркетплейс при проверке лицензий приложений.

Что происходит, когда стендов становится два

Копия портала — это копия базы. Вместе с базой переезжает и опция service_client_id, а значит, у тестового стенда тот же member_id, что у боевого. Для всех облачных сервисов Битрикса и для всех приложений это один и тот же портал.

Со стороны приложения картина такая. Портал в базе приложения хранится одной записью, и ключ у неё — member_id. Вот типичная схема таблицы:

$table->string('member_id', 64)->unique();   // идентификатор портала
$table->string('domain')->nullable();
$table->text('access_token')->nullable();
$table->text('refresh_token')->nullable();
$table->string('application_token', 64)->nullable();
$table->string('client_endpoint')->nullable();

При установке приложение ищет портал по member_id и обновляет запись. Когда тот же идентификатор приходит со второго адреса, перезаписывается всё: домен, токены доступа, адрес REST-эндпоинта и токен, которым проверяются входящие события.

Дальше начинается то, за что клиент и называет приложение сломанным.

  • Фоновые задачи уходят не туда. Приложение обращается к порталу по сохранённому адресу, а там теперь тестовый домен. Боевые данные не обновляются.
  • Боевой портал получает ошибки авторизации. Его токены в базе приложения затёрты токенами стенда.
  • События перестают приниматься. Приложение сверяет входящее событие с сохранённым токеном безопасности, а он уже от другой установки.
  • Лицензия делится на двоих. Подписка в Маркетплейсе привязана к member_id, и два портала выглядят как один. Кто последним продлил или установил, того настройки и действуют.

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

Как проверить за минуту

Открыть на обоих порталах «Настройки — Инструменты — Командная PHP-строка» и выполнить:

\Bitrix\Main\Loader::includeModule('rest');
echo COption::GetOptionString("rest", "service_client_id");

Если значения совпали — диагноз подтверждён. Можно сразу посмотреть и сам идентификатор, который видят приложения:

echo \CRestUtil::getMemberId();

Как сменить member_id на копии

Идея простая: заставить тестовый портал заново зарегистрироваться в сервисе авторизации и получить новый client_id. Важная деталь — при повторной регистрации портал передаёт текущий member_id, если он есть, и тогда сервис вернёт прежний идентификатор. Поэтому сначала нужно стереть старые значения, и только потом регистрироваться.

Перед тем как выполнять, убедитесь, что вы на копии. Проверьте адрес в строке браузера дважды: на боевом портале эти команды отвяжут приложения у всех сразу.

\Bitrix\Main\Loader::includeModule('rest');

// стираем прежнюю регистрацию
COption::RemoveOption("rest", "service_client_id");
COption::RemoveOption("main", "server_uniq_id");
\Bitrix\Rest\OAuthService::getEngine()->clearAccess();

// получаем новую
\Bitrix\Rest\OAuthService::register();

// проверяем, что идентификатор сменился
echo COption::GetOptionString("rest", "service_client_id");

Второй строкой стирается server_uniq_id — отдельный идентификатор установки из главного модуля. Он тоже дублируется при копировании, и его стоит сбросить, но прежде стоит знать, на что он влияет. В ядре он участвует в трёх местах: системе обновлений, именах блокировок в MySQL (md5($unique).md5($name) — две установки с одинаковым значением на общей базе будут мешать друг другу) и, что важнее всего, в защитном токене форм:

function bitrix_sessid_val()
{
    return md5(CMain::GetServerUniqID() . $session->getId());
}

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

Если register() завершился с ошибкой, смотрите на текст: запрос подписывается лицензионным ключом, и отказ обычно означает проблему с лицензией, а не с кодом.

Что происходит после смены

Ядро не выбрасывает старые значения молча, и это полезно знать.

  • Прежние client_id и client_secret сохраняются в опциях с именами вида 1763000000.0000 service_client_id — с отметкой времени в начале. Если что-то пойдёт не так, предыдущее значение можно найти.
  • В журнал событий пишется запись REST_OAUTH_REGISTER с критическим уровнем и ссылкой на эти опции. По ней видно, когда и сколько раз портал перерегистрировали.
  • Приложения на копии нужно переустановить. Их токены выпускались для старого client_id и больше не действуют. Заодно это честный способ проверить, что стенд стал отдельным порталом.

Как делать стенд, чтобы не наступать на это

  1. Менять идентификатор сразу после разворачивания копии, до того как кто-нибудь откроет на ней приложение. Это одна команда, и она экономит день разбирательств.
  2. Сменить адрес портала в настройках главного модуля. Если копия продолжает считать себя боевым доменом, часть ссылок и обратных вызовов пойдёт на боевой сайт.
  3. Отключить на стенде исходящую почту и телефонию, иначе тестовые письма уйдут настоящим клиентам.
  4. Добавить проверку в регламент переноса. Копию разворачивают редко, и помнит о member_id обычно только тот, кто уже обжёгся.

Если копия уже поработала с боевыми приложениями

Порядок такой. Сначала сменить member_id на копии — иначе всё повторится. Затем на боевом портале переустановить приложения, которые перестали работать: при установке они пришлют свежие токены и правильный домен, и запись в базе приложения вернётся к боевому порталу.

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

Если приложение на вашем портале ведёт себя странно, а совпадений по датам с разворачиванием стенда не находится, напишите нам: разберём, что именно ломается. Мы сами разрабатываем приложения для Маркетплейса и смотрим на такие истории с обеих сторон — и со стороны портала, и со стороны приложения.

Статья была полезна?

Оцените первым — так другие читатели поймут, стоит ли читать

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

Почему приложения ломаются именно на боевом портале, а не на копии?

Потому что приложение хранит портал одной записью с ключом member_id. Тестовый стенд с тем же идентификатором при установке перезаписывает в ней домен и токены — и приложение начинает обращаться к стенду вместо боевого портала. На самой копии при этом всё выглядит работающим.

Где в коробке хранится member_id?

Прямо он не хранится: для коробки это md5 от значения опции service_client_id модуля rest. Посмотреть можно командой COption::GetOptionString("rest", "service_client_id"), а итоговый идентификатор — через \CRestUtil::getMemberId().

Достаточно ли просто переустановить приложения на копии?

Нет. Переустановка выдаёт новые токены, но member_id остаётся прежним, и порталы по-прежнему выглядят одним. Сначала меняется идентификатор, потом переустанавливаются приложения.

Что делает удаление server_uniq_id?

Это идентификатор установки из главного модуля. На нём держатся имена блокировок в MySQL, запросы к системе обновлений и защитный токен форм sessid: он считается как md5(server_uniq_id + идентификатор сессии). Поэтому после удаления опции у работающих на портале людей формы начнут ругаться, пока они не перезайдут. Новое значение создаётся автоматически при первом обращении.

Можно ли вернуть прежний идентификатор, если что-то пошло не так?

Старые значения client_id и client_secret остаются в опциях модуля rest под именами с отметкой времени, а факт перерегистрации пишется в журнал событий. Восстановление делается вручную и только на том портале, которому эти значения принадлежали.

Нужно ли менять member_id, если стенд развёрнут из чистого дистрибутива?

Нет. Чистая установка регистрируется в сервисе сама и получает собственный идентификатор. Проблема возникает только при копировании базы существующего портала.