Клиент пишет: приложение из Маркетплейса перестало работать. Переустановка не помогает, токены на месте, портал жив. Через полчаса разговора выясняется, что неделю назад подняли тестовый стенд — развернули копию боевого портала на другом домене. В этом и дело.
Разбираем, почему копия коробочного Битрикс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 и больше не действуют. Заодно это честный способ проверить, что стенд стал отдельным порталом.
Как делать стенд, чтобы не наступать на это
- Менять идентификатор сразу после разворачивания копии, до того как кто-нибудь откроет на ней приложение. Это одна команда, и она экономит день разбирательств.
- Сменить адрес портала в настройках главного модуля. Если копия продолжает считать себя боевым доменом, часть ссылок и обратных вызовов пойдёт на боевой сайт.
- Отключить на стенде исходящую почту и телефонию, иначе тестовые письма уйдут настоящим клиентам.
- Добавить проверку в регламент переноса. Копию разворачивают редко, и помнит о 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, если стенд развёрнут из чистого дистрибутива?
Нет. Чистая установка регистрируется в сервисе сама и получает собственный идентификатор. Проблема возникает только при копировании базы существующего портала.