Безопасный переход из персональной ссылки в клиентские сервисы
Задача
Внешняя система формировала персональные ссылки для клиентов. После перехода человек должен был без повторного поиска нужного раздела попасть в один из нескольких интерфейсов: форму дозаполнения профиля, личный кабинет, конкретную страницу кабинета или специализированную веб-платформу с уже созданной сессией.
За короткой ссылкой скрывался составной серверный сценарий. Сначала требовалось подтвердить, что запрос относится к существующему клиенту. Затем — получить актуальный профиль и связанные учётные записи, определить полноту обязательных данных, выбрать маршрут с учётом языка и назначения ссылки, запросить одноразовый вход у нужного провайдера и безопасно выполнить внешний редирект.
Публичная точка входа при этом фактически участвует в авторизации. Одних идентификаторов клиента в URL недостаточно: их можно подобрать, изменить или повторно использовать. Решение должно было проверять происхождение ссылки, ограничивать срок её действия и не допускать повторного применения, подмены маршрута, утечки токенов в журнал или перенаправления на произвольный домен.
Роль и объём
Спроектировал и реализовал отдельный плагин WordPress, который работает как шлюз между входящей персональной ссылкой, CRM API, сервисом единого входа и несколькими клиентскими интерфейсами.
В работу вошли публичные маршруты, контракт подписанной ссылки, проверка клиента по актуальным данным CRM, выбор целевой учётной записи, получение стандартной или специализированной ссылки входа, маршрутизация незаполненного профиля, контроль внешних адресов, ограничение частоты запросов, одноразовое использование ссылки и диагностический контур без записи секретов.
Отдельно предусмотрел переход со старого формата ссылок: совместимость включается явным флагом только на время согласованной миграции источников. Без настроенного ключа подписи новый защищённый режим закрывает публичный вход, а не продолжает работу с ослабленной проверкой.
Ограничения
- Точка входа доступна из интернета и по результату может создать авторизованную сессию во внешнем сервисе. Все параметры запроса нужно считать недоверенными.
- Клиентские данные и состояние профиля хранятся во внешней CRM. Недоступность или некорректный ответ API не должны превращаться в разрешение входа.
- Для разных сценариев используются разные целевые системы. Адрес может прийти из CRM, из ответа SSO-сервиса или из административной настройки WordPress.
- Одна персональная ссылка не должна оставаться бессрочным многоразовым способом входа.
- Сайт может работать за CDN или reverse proxy. Заголовки с исходным IP нельзя принимать от любого клиента без проверки доверенного прокси.
- Секреты интеграций и выдаваемые токены проходят через серверную часть, поэтому подробный журнал HTTP-запросов сам по себе создаёт риск утечки.
- Изменение формата ссылки требует синхронного обновления системы, которая её формирует; для такого изменения нужен управляемый миграционный период.
Архитектура
Персональная ссылка из внешней системы
│
▼
Публичный маршрут WordPress
├── Лимит запросов по проверенному IP
├── Дополнительная проверка списка блокировок
└── Проверка подписанного запроса
├── HMAC-SHA256 по каноническому набору параметров
├── Короткий срок действия
├── Криптографический nonce
└── Защита от повторного использования
│
▼
Проверка клиента в CRM
├── Получение профиля по идентификатору
├── Сверка email в постоянное время
├── Проверка обязательных полей профиля
└── Получение связанных учётных записей
│
┌─────────┼──────────────────────┐
│ │ │
▼ ▼ ▼
Профиль неполон Обычный вход Вход в веб-платформу
│ │ │
▼ ▼ ▼
Форма по языку Ссылка из CRM Выбор актуальной записи
и запрос SSO-токена
│ │ │
└─────────┴──────────┬───────────┘
▼
Контроль конечного редиректа
├── Только HTTPS
├── Разрешённый домен
├── Ограниченные route-параметры
└── Безопасный запасной маршрут
│
▼
Клиентский интерфейс
CRM API / SSO API
├── Тайм-ауты и проверка HTTP-статуса
├── Проверка JSON и бизнес-ответа
├── Запасной сценарий при отказе SSO
└── Журнал событий без headers, body и токенов
Маршрут не доверяет ссылке только потому, что в ней присутствуют email и номер учётной записи. Сначала проверяются подпись и время, после чего сервер повторно получает профиль из CRM и сопоставляет данные. Одноразовый nonce расходуется атомарно до выдачи внешней сессии, поэтому два параллельных запроса не могут штатно использовать одну ссылку как два независимых входа.
Реализация
Входная ссылка подписывается HMAC-SHA256 общим секретом, который хранится в серверной конфигурации и не передаётся браузеру. В подпись входят маршрут, идентификаторы клиента, язык, целевой раздел, тип платформы, актив, срок действия и nonce. Параметры приводятся к фиксированному порядку и кодируются по одному правилу, поэтому удаление, добавление или изменение любого значимого значения делает подпись недействительной.
Срок жизни ссылки ограничен, дополнительно проверяется максимально допустимое время в будущем. Это закрывает сценарий, в котором источник по ошибке или намеренно выпускает почти бессрочную ссылку. После проверки личности nonce записывается через атомарное добавление уникальной опции WordPress. Повторный переход получает общий отказ, а отдельное cron-событие удаляет истёкший технический маркер.
После криптографической проверки шлюз запрашивает профиль клиента в CRM и сопоставляет email из ссылки с актуальным значением через hash_equals. Только затем начинается бизнес-маршрутизация. Если в профиле отсутствуют дата рождения, адрес, город или индекс, клиент направляется на форму нужной языковой версии. Домены этих форм берутся из настроек, но перед редиректом всё равно проходят тот же список разрешённых адресов.
Для обычного кабинета CRM выпускает новую ссылку входа. Если персональная ссылка ведёт в специализированную платформу, сервис получает связанные учётные записи, предпочитает последнюю актуальную рабочую запись, извлекает её внешний идентификатор и обменивает его на SSO-токен. Если специализированный SSO временно недоступен или не вернул токен, предусмотрен переход к стандартному сценарию входа.
Параметры глубокого перехода ограничены по формату и длине. Если для перехода во внутренний раздел кабинета нужно раскрыть промежуточную ссылку, сервер выполняет HEAD-запрос без автоматического следования по цепочке. Исходный адрес и полученный Location проверяются по схеме и домену. Это одновременно ограничивает SSRF и не позволяет превратить сервис в открытый редиректор.
Частота обращений ограничивается по хешу IP. Заголовки CDN и reverse proxy учитываются только тогда, когда непосредственный адрес соединения входит в настроенный список доверенных прокси; иначе используется REMOTE_ADDR. IP-блокировка остаётся дополнительным фильтром, а основная проверка доступа строится на подписи, сроке действия, одноразовости и сверке с CRM.
Административные настройки доступны только пользователям с manage_options. Для оставшегося вспомогательного SQL-адаптера имена колонок и выбираемых полей ограничены жёстким списком, поэтому параметризация значения не создаёт ложного ощущения безопасности при динамических идентификаторах SQL.
Сбои и диагностика
HTTP-клиент различает транспортную ошибку, неподходящий HTTP-статус, повреждённый JSON и бизнес-ошибку провайдера. У запросов задан ограниченный тайм-аут. При недоступности CRM автоматический вход не создаётся; клиент переводится на обычную страницу входа. Отказ специализированного SSO не блокирует весь путь: шлюз пробует получить стандартную ссылку кабинета.
Предыдущая реализация могла записать в файл внутри каталога плагина аргументы HTTP-запроса вместе с Authorization, API-ключом и телом. Этот журнал удалён. Диагностика отправляется в серверный PHP-журнал и содержит категорию операции, домен и путь без query string, HTTP-статус, безопасный код ошибки или идентификатор запроса провайдера. Заголовки, тела ответов и запросов, SSO-токены и подписи целиком не сохраняются; перед записью дополнительно маскируются известные форматы секретов и email.
Ошибки подписи, истёкший срок, повторный nonce, несовпадение клиента и превышение лимита имеют разные внутренние коды, но пользователю возвращается общее сообщение. Так диагностика остаётся полезной для эксплуатации и не превращается в подсказку для перебора.
границы решения
HMAC подтверждает происхождение и целостность ссылки, но не шифрует её параметры. Ссылка передаётся только по HTTPS; на уровне CDN, reverse proxy, веб-сервера и аналитики требуется отключить запись query string либо маскировать идентификаторы. Если политика проекта запрещает присутствие персональных данных в URL, следующим шагом должен быть непрозрачный одноразовый код со связанной серверной записью.
Одноразовость хранится в базе WordPress и очищается через WP-Cron. Для очень большого потока ссылок этот слой следует перенести в хранилище с атомарной операцией add-if-absent и автоматическим TTL, например Redis. Ограничитель частоты на transient также становится строго согласованным только при общем постоянном object cache для всех экземпляров приложения.
Список разрешённых доменов защищает границу WordPress, но изменение домена кабинета или SSO требует обновления конфигурации. Режим старых неподписанных ссылок предусмотрен только для согласованной миграции и должен быть выключен после обновления всех источников. Само переключение требует интеграционной проверки с реальными CRM и SSO: локальный тест подтверждает подпись, защиту от изменения параметров и повторного nonce, но не заменяет проверку контрактов внешних систем.
Результат
Вместо набора условных редиректов получился единый серверный шлюз входа. Он принимает решение по актуальным данным CRM, поддерживает несколько клиентских маршрутов и сохраняет удобный переход в нужный раздел, но не считает знание email и номера учётной записи достаточным подтверждением доступа.
Ссылка имеет проверяемое происхождение, ограниченный срок и одноразовый идентификатор. Внешние адреса проходят список разрешённых доменов, входные маршруты ограничены по частоте, ответы интеграций проверяются на транспортном и прикладном уровне, а отказ одной целевой системы имеет контролируемый запасной сценарий. Диагностика позволяет различать причины сбоя без публикации токенов и API-ключей.
Технический результат подтверждается реализованным контрактом подписи, атомарной защитой от повторного использования, проверкой редиректов и тестами негативных сценариев.
Стек
PHP · WordPress · CRM API · SSO · HMAC-SHA256 · WP HTTP API · ACF · MySQL · WP-Cron
Есть похожая задача?
Расскажите о текущей системе и нужных изменениях — обсудим следующий шаг.
Следующий кейс: Конфигурируемая система промо-цен для WooCommerce