Изоляция данных нескольких организаций в PHP не решается добавлением условия WHERE organization_id = ? на главном экране. Утечка может возникнуть в API, экспорте, кэше, вложении, потребителе очереди или запланированном процессе. Она также может произойти, когда легитимный администратор переключается между организациями, а система сохраняет предыдущий контекст.
Архитектурная цель должна быть ясной: ни одна операция, которая читает, изменяет, обрабатывает или передаёт клиентскую информацию, не должна выполняться без проверяемого контекста организации. Этот контекст должен явно передаваться и валидироваться на каждой значимой границе приложения.
Что должно изолировать SaaS-приложение

Транзакционная модель — лишь часть поверхности риска. Инвентаризируйте ресурсы, имеющие организационного владельца, и определите для каждого из них, как идентифицируется, хранится, извлекается, удаляется и аудируется его принадлежность.
- Транзакционные данные: пользователи, проекты, заказы, счета, конфигурации и связи между сущностями.
- Файлы и вложения: объекты во внешнем хранилище, миниатюры, сгенерированные документы и их метаданные.
- Кэш: результаты запросов, вычисленные разрешения, сессии, ответы API и данные конфигурации.
- Поисковые индексы: индексированные документы, подсказки и предварительно агрегированные фильтры.
- Асинхронная обработка: задачи очереди, повторные попытки, пакеты импорта и уведомления.
- Эксплуатация и наблюдаемость: логи, трассировки, метрики, экспорты для поддержки и внутренние инструменты.
Не для всех ресурсов нужна одинаковая стратегия. Публичный каталог может быть общим, тогда как счёт, его PDF и логи скачивания должны сохранять однозначную связь с организацией. Решение следует документировать, чтобы новая сущность не появилась без правил владения.
Выбор модели изоляции данных
Существуют три распространённые модели. Нет одной, универсально лучшей: выбор зависит от регуляторных требований, объёма, эксплуатации, коммерческой модели и способности команды поддерживать платформу.
Общая база данных с ключом организации
Все организации используют общие таблицы, и каждая запись, подлежащая изоляции, содержит ключ вроде organization_id. Это наиболее прямой подход для развития продукта и выполнения глобальных агрегированных запросов. Взамен он требует предельной дисциплины: каждый запрос, связь, индекс, кэш и задача должны учитывать контекст организации.
Как минимум, используйте внешние ключи, где это применимо, составные индексы, начинающиеся с organization_id, и также составные уникальные ограничения. Например, код заказа, уникальный в рамках организации, не должен объявляться глобально уникальным, если это не является бизнес-правилом.
Отдельная схема для каждой организации
Каждый клиент работает в отдельной логической схеме на одном сервере базы данных. Это уменьшает риск пропустить фильтр в разделённых таблицах, но усложняет миграции, подключения, аналитические инструменты и глобальные запросы. Такой подход подходит, только если СУБД, фреймворк и повседневная эксплуатация последовательно поддерживают этот паттерн.
База данных для каждой организации
Разделение баз данных обеспечивает более строгую границу и может упростить восстановление или перемещение отдельных клиентов. Оно также увеличивает число подключений, миграций, резервных копий, мониторинга и развёртываний изменений структуры. Особенно важно оценить, как будут выполняться глобальные отчёты, массовые изменения и восстановление после ошибок.
Физическое разделение сокращает некоторые классы сбоев, но не заменяет авторизацию, контроль файлов, управление секретами или валидацию контекста в общих сервисах.
Эталонная архитектура: явный контекст на границах
Контекст организации не следует выводить из произвольных параметров, отправленных браузером. Его нужно определять на основе аутентифицированного и авторизованного источника: валидированного поддомена, токена с подходящей аудиторией, членства пользователя или учётных данных интеграции, связанных только с одной организацией.
В PHP-приложении входной слой может создавать неизменяемый объект контекста с идентификатором организации, актором, его разрешениями и идентификатором запроса. Контроллеры, консольные команды и потребители очередей получают этот контекст или восстанавливают его по проверенным данным. Избегайте изменяемых глобальных переменных, которые могут некорректно сохраняться в долгоживущих процессах.
final class OrganizationContext {
public function __construct(
public readonly string $organizationId,
public readonly string $actorId
) {}
}Репозитории должны требовать контекст для запроса или изменения изолированных сущностей. Интерфейс, при котором сложно его пропустить, предпочтительнее неявного соглашения, зависящего от памяти каждого разработчика. Где возможно, также применяйте политики доступа на уровне домена: принадлежность к организации не даёт автоматически права на любое действие внутри неё.
Как избежать забытых фильтров в запросах и связях
Изолированный запрос должен фильтровать по организации до поиска по бизнес-идентификаторам. Сначала получить запись по id, а затем проверить её владельца может привести к раскрытию данных, если результат сериализуется, записывается в лог или используется до его отклонения.
- Централизуйте запросы в репозиториях или сервисах чтения с методами, принимающими контекст.
- Запретите прямой доступ к изолированным моделям из контроллеров, шаблонов и потребителей событий.
- Проверяйте связи: связь, загружаемая отложенно, может обойти фильтр, применённый к основной сущности.
- Используйте ограничения базы данных, чтобы предотвращать связи между строками разных организаций, когда это позволяет модель.
- Определите соглашения для миграций, тестовых seed-данных и аналитических запросов.
В СУБД, предлагающих политики безопасности на уровне строк, они могут обеспечить дополнительную защиту. Однако их внедрение должно включать тесты подключения, управление ролями и проверку административных процессов. Не следует считать, что политика базы данных автоматически защищает файлы, кэш или внешние индексы.
Риски вне основного веб-потока
Непрозрачные идентификаторы уменьшают возможность перечисления, но не авторизуют доступ. UUID или случайный идентификатор всё равно должен сопоставляться с текущей организацией. Аналогично, подписанный URL для скачивания требует объекта, принадлежащего нужной организации, подходящего срока действия и правил отзыва при изменении разрешений.
Ключи кэша должны включать идентификатор организации и, когда содержимое зависит от разрешений, дополнительное измерение роли или версии авторизации. Ключ вида dashboard:summary небезопасен в среде с несколькими организациями; ключ с явным контекстом также позволяет выполнять более точную инвалидацию.
Экспорты особенно чувствительны, поскольку обычно выполняются вне исходного запроса. Сохраняйте, кто его запросил, для какой организации, какие фильтры были одобрены и куда будет доставлен результат. Не отправляйте вложения или ссылки получателям, вычисленным на основе непроверенных данных.
Распространение контекста в API, webhooks и очередях
API должен определять организацию по учётным данным или проверять, что запрошенный ресурс принадлежит организации, связанной с этими учётными данными. Разрешение заголовка X-Organization-Id может быть допустимо для операторов с явным делегированием, но требует специальной авторизации, аудита и интерфейса, делающего смену контекста видимой.
Входящие webhooks не должны доверять идентификатору организации в теле запроса без проверки подписи, отправителя и предварительно установленной связи интеграции. Для исходящих webhooks генерируйте события из уже ограниченных данных и не переиспользуйте содержимое сообщения из общей очереди без проверки получателя.
Каждая асинхронная задача должна передавать идентификатор организации вместе с идентификатором ресурса и восстанавливать контекст до выполнения запроса. Потребитель должен проверять оба значения, даже если задача была создана внутри системы. Повторные попытки, отложенные задачи и запланированные задания требуют того же правила: не существует неявного контекста запроса, доступного безопасным образом.
Проверяемые тесты и диагностические сигналы
Самый важный тест заключается не в том, что организация видит собственные данные, а в том, что она не может читать или изменять данные другой. Создайте две организации с намеренно похожими данными и выполняйте интеграционные тесты для каждой точки входа: веб-интерфейса, API, команд, экспортов, скачиваний и потребителей очередей.
- Запросите ресурс организации B, используя сессию или учётные данные организации A, и ожидайте ответ, не раскрывающий информацию.
- Попытайтесь обновить, удалить, скачать и экспортировать ресурсы другой организации, а не только запросить их.
- Проверьте, что ключи кэша A и B формируют независимые результаты.
- Запустите задачу очереди с ресурсом другой организации и убедитесь, что она контролируемо завершается ошибкой.
- Протестируйте восстановления, импорты и ночные задачи с данными более чем одной организации.
- Логируйте чувствительные действия с актором, организацией, ресурсом и результатом, не добавляя в логи ненужные персональные данные.
Тесты на основе свойств могут дополнить ручные сценарии: для любого ресурса, созданного в рамках организации, ни один актор без действительного членства не должен иметь возможность наблюдать или изменять его через доступный извне маршрут. Это свойство должно применяться к будущим изменениям конечных точек и репозиториев.
План внедрения для существующего приложения
Если данные уже смешаны, не начинайте с переписывания всего приложения. Сначала инвентаризируйте сущности, потоки, интеграции и административные доступы. Затем определите владение каждой записью и разрешите неоднозначные случаи с помощью проверяемых бизнес-правил.
- Добавьте сущность организации и ключ принадлежности в целевые таблицы.
- Заполните этот ключ посредством контролируемой миграции и сохраните документальные подтверждения случаев без надёжного назначения.
- Внедрите репозитории с ограничением по организации и тесты перекрёстного доступа на наиболее чувствительных маршрутах.
- Включите контекст организации в кэш, файлы, поиск и новые задачи.
- Постепенно мигрируйте устаревшие потоки и блокируйте новые запросы без контекста при проверке кода.
- Активируйте более строгие меры контроля, когда метрики и тесты подтвердят достаточное покрытие.
Решения, которые стоит документировать до роста

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



