Signed requests

Подписанные запросы (Signed Requests) используются для контроля доступа к ресурсам, которые загружаются картой. В контексте Mapbox GL JS этот механизм позволяет добавлять специальные параметры аутентификации к запросам тайлов, стилей, шрифтов, изображений, GeoJSON-данных и других ресурсов.

Подписывание запросов особенно важно в случаях:

  • работы с защищёнными API;
  • доступа к приватным картографическим данным;
  • интеграции с корпоративными системами авторизации;
  • использования временных токенов доступа;
  • реализации многоуровневой безопасности.

В отличие от стандартного использования публичного access token, подписанные запросы позволяют гибко контролировать доступ к каждому отдельному ресурсу.


Как Mapbox GL JS выполняет сетевые запросы

При отображении карты библиотека автоматически выполняет множество HTTP-запросов:

  1. Загрузка стиля.
  2. Получение источников данных.
  3. Загрузка векторных тайлов.
  4. Получение растровых изображений.
  5. Загрузка шрифтов.
  6. Получение спрайтов.

Например:

/styles/v1/example/style
/tiles/12/2345/1234.pbf
/fonts/v1/Arial Unicode/0-255.pbf
/sprites/sprite.json

По умолчанию каждый запрос выполняется без дополнительной модификации, кроме передачи access token, если используются сервисы Mapbox.

Для вмешательства в процесс формирования запроса применяется механизм transformRequest.


Механизм transformRequest

Параметр transformRequest позволяет перехватывать каждый запрос перед его отправкой.

Общий синтаксис:

const map = new mapboxgl.Map({
    container: 'map',
    style: 'mapbox://styles/mapbox/streets-v12',

    transformRequest: (url, resourceType) => {
        return {
            url: url
        };
    }
});

Функция получает:

  • url — исходный адрес ресурса;
  • resourceType — тип загружаемого ресурса.

Возвращаемый объект определяет итоговые параметры запроса.


Типы ресурсов

Аргумент resourceType позволяет различать категории загружаемых данных.

Наиболее распространённые значения:

Тип Описание
Style JSON-стиль карты
Source Источник данных
Tile Векторный или растровый тайл
Glyphs Шрифты
SpriteImage Изображение спрайта
SpriteJSON Описание спрайта
Image Дополнительные изображения

Пример проверки:

transformRequest: (url, resourceType) => {
    if (resourceType === 'Tile') {
        console.log(url);
    }

    return { url };
}

Такой подход позволяет подписывать только необходимые запросы.


Добавление токена в строку запроса

Самый простой вариант подписывания — добавление параметров в URL.

Исходный адрес:

https://api.example.com/tiles/10/500/300.pbf

После модификации:

https://api.example.com/tiles/10/500/300.pbf?token=abc123

Реализация:

transformRequest: (url) => {
    const signedUrl =
        `${url}?token=abc123`;

    return {
        url: signedUrl
    };
}

Однако такой подход подходит только для простых схем авторизации.


Добавление нескольких параметров подписи

Часто сервер ожидает несколько параметров:

  • идентификатор клиента;
  • срок действия подписи;
  • цифровую подпись.

Пример:

transformRequest: (url) => {
    const expires = Date.now() + 60000;

    const signedUrl =
        `${url}?client=42&expires=${expires}&signature=XYZ`;

    return {
        url: signedUrl
    };
}

На стороне сервера выполняется проверка подписи и времени действия ссылки.


Использование URL API

Для корректной работы с параметрами рекомендуется использовать объект URL.

transformRequest: (url) => {
    const requestUrl = new URL(url);

    requestUrl.searchParams.set(
        'token',
        'abc123'
    );

    return {
        url: requestUrl.toString()
    };
}

Преимущества:

  • отсутствие ошибок конкатенации строк;
  • автоматическое кодирование параметров;
  • удобная работа с существующими query-параметрами.

Передача заголовков авторизации

Многие API используют HTTP-заголовки вместо параметров URL.

Например:

Authorization: Bearer token

Реализация:

transformRequest: (url) => {
    return {
        url,
        headers: {
            Authorization:
                'Bearer my-secret-token'
        }
    };
}

После этого каждый запрос будет содержать указанный заголовок.


Использование JWT

Одним из наиболее распространённых способов подписывания запросов является применение JWT.

Пример:

const jwtToken =
    localStorage.getItem('jwt');

const map = new mapboxgl.Map({
    container: 'map',
    style: styleUrl,

    transformRequest: (url) => {
        return {
            url,
            headers: {
                Authorization:
                    `Bearer ${jwtToken}`
            }
        };
    }
});

JWT может содержать:

  • идентификатор пользователя;
  • список разрешений;
  • срок действия токена;
  • сведения о роли пользователя.

Подписывание только определённых доменов

Не рекомендуется отправлять секретные данные на все адреса.

Правильнее ограничивать область действия подписи.

transformRequest: (url) => {
    if (
        url.startsWith(
            'https://api.company.com'
        )
    ) {
        return {
            url,
            headers: {
                Authorization:
                    'Bearer token'
            }
        };
    }

    return { url };
}

Такой подход предотвращает утечку токенов сторонним сервисам.


Временные подписи

Часто сервер генерирует ссылки с ограниченным сроком действия.

Пример:

https://api.company.com/tile.pbf
?expires=1710000000
&signature=8d2f2c4d

Сервер проверяет:

  1. Не истекло ли время действия.
  2. Совпадает ли цифровая подпись.
  3. Имеет ли пользователь доступ к данным.

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


Генерация подписи на клиенте

Иногда подпись вычисляется непосредственно в браузере.

Пример схемы:

const timestamp =
    Date.now().toString();

const signature =
    generateSignature(
        url,
        timestamp
    );

Далее:

transformRequest: (url) => {
    return {
        url:
            `${url}?ts=${timestamp}` +
            `&sig=${signature}`
    };
}

Однако хранение секретного ключа на клиенте считается небезопасным решением.


Серверное подписывание

Наиболее безопасный вариант предполагает генерацию подписей на сервере.

Схема работы:

Browser
   |
   | Запрос подписи
   v
Backend
   |
   | Генерация подписи
   v
Signed URL
   |
   | Использование в Mapbox GL JS
   v
Protected Resource

Преимущества:

  • секретный ключ не покидает сервер;
  • возможна централизованная политика доступа;
  • проще реализовать аудит.

Подписывание GeoJSON-источников

Источник данных может быть защищён авторизацией.

Пример:

map.addSource('cities', {
    type: 'geojson',
    data:
        'https://api.company.com/cities'
});

Подписывание:

transformRequest: (url, type) => {
    if (type === 'Source') {
        return {
            url,
            headers: {
                Authorization:
                    'Bearer token'
            }
        };
    }

    return { url };
}

В результате GeoJSON будет загружаться только после успешной проверки прав доступа.


Подписывание тайлов

Чаще всего защита применяется именно к тайлам.

Пример:

transformRequest: (
    url,
    resourceType
) => {
    if (resourceType === 'Tile') {
        return {
            url,
            headers: {
                Authorization:
                    'Bearer token'
            }
        };
    }

    return { url };
}

При перемещении карты каждый тайл автоматически получает подпись.


Использование cookies

Некоторые системы безопасности используют авторизационные cookie.

Для передачи cookie необходимо включить учётные данные.

transformRequest: (url) => {
    return {
        url,
        credentials: 'include'
    };
}

Теперь браузер будет отправлять:

Cookie: SESSION_ID=12345

для соответствующего домена.


Режим credentials

Поддерживаются несколько режимов.

omit

Cookie не отправляются.

credentials: 'omit'

same-origin

Cookie передаются только для того же источника.

credentials: 'same-origin'

include

Cookie отправляются всегда.

credentials: 'include'

Выбор режима зависит от политики безопасности приложения.


Поддержка CORS

Подписанные запросы часто выполняются между разными доменами.

Сервер должен разрешить:

Access-Control-Allow-Origin

а также:

Access-Control-Allow-Headers

Например:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Headers: Authorization

Без корректной настройки CORS браузер заблокирует запрос ещё до его выполнения.


Проверка срока действия токена

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

Пример:

function getToken() {
    if (tokenExpired()) {
        refreshToken();
    }

    return currentToken;
}

Далее:

transformRequest: (url) => {
    return {
        url,
        headers: {
            Authorization:
                `Bearer ${getToken()}`
        }
    };
}

Таким образом карта продолжает работать даже после обновления токена.


Централизованный менеджер авторизации

В крупных приложениях логика подписывания обычно выносится в отдельный модуль.

class AuthManager {

    getToken() {
        return this.token;
    }

    getHeaders() {
        return {
            Authorization:
                `Bearer ${this.token}`
        };
    }
}

Использование:

const auth =
    new AuthManager();

transformRequest: (url) => {
    return {
        url,
        headers:
            auth.getHeaders()
    };
}

Подобная архитектура упрощает сопровождение проекта.


Отладка подписанных запросов

Для анализа сетевой активности используются инструменты разработчика браузера.

Проверяются:

  • итоговый URL;
  • параметры подписи;
  • заголовки;
  • код ответа сервера;
  • сообщения CORS;
  • время жизни токена.

Полезно контролировать наличие:

401 Unauthorized

и

403 Forbidden

Код 401 обычно означает отсутствие корректной аутентификации, а 403 свидетельствует о недостатке прав доступа.


Практические рекомендации

Не передавать секретные ключи в клиентском коде.

Даже минифицированный JavaScript может быть изучен и декомпилирован.

Подписывать только необходимые запросы.

Не следует добавлять токены к сторонним ресурсам.

Использовать короткое время жизни подписей.

Временные URL значительно снижают риск компрометации.

Применять HTTPS.

Передача токенов по HTTP создаёт угрозу их перехвата.

Регулярно обновлять токены.

Механизм refresh token позволяет поддерживать непрерывную работу карты без повторной аутентификации пользователя.

Логировать операции проверки доступа.

Серверные журналы позволяют выявлять попытки несанкционированного использования картографических ресурсов.

Комбинировать несколько механизмов защиты.

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

  • HTTPS;
  • JWT;
  • временные подписи;
  • серверная валидация;
  • ограничение доступа по ролям;
  • аудит запросов.

Такой подход обеспечивает безопасную работу Mapbox GL JS с приватными стилями, тайлами и пользовательскими геоданными в корпоративных и высоконагруженных веб-приложениях.