Аутентификация по токенам, подписанные URL и защита origin

Автор: Николай СапуновОбновлено: август 202613 мин чтения
Содержание статьи +

TL;DR

Аутентификация по токенам и подписанные URL позволяют CDN решать в каждом запросе, имеет ли зритель право скачать манифест, ключ или сегмент – и при этом origin никогда не видит этого пользователя. Математика везде одна: на CloudFront, Akamai, Cloudflare, Google Media CDN, Fastly и Mux короткая строка фактов (URL, срок действия, по желанию IP или гео) хэшируется с секретом и прикрепляется к запросу. Защита origin – вторая половина задачи: единственным маршрутом до источника должен быть CDN, иначе подписанный URL – замок на двери, к которой никто не обязан подходить. Коллизии cache key с параметрами подписи и истечение токена посреди сессии – две самые частые причины «тихих» отказов в проде.

Зачем это нужно

У вас есть стриминговый продукт. Часть контента платная, часть гео-ограничена, часть доступна только залогиненным. Проверка «можно ли этому пользователю?» на origin работает на десять тысяч просмотров в день и ломается на миллион в минуту. Каждый серьёзный CDN решает это подписью URL на edge – и каждая серьёзная команда хотя бы раз делает это неправильно. Эта статья – для инженера, который скоупит такую работу, продакта, сравнивающего вендоров, и архитектора, которому сказали «просто включите signed URLs», не объяснив, что сломается дальше.

Что такое подписанный URL на самом деле

Подписанный URL – это обычный HTTP URL плюс три дополнительных факта: policy (что разрешено владельцу), expiry (когда разрешение заканчивается) и signature (криптографический хэш, связывающий policy и expiry с секретом, известным только вам и CDN). URL может прочитать кто угодно. Подделать новый без секрета не может никто.

В проде встречаются две схемы подписи: HMAC-SHA256 и RSA-SHA256 (в мире JWT – RS256). HMAC использует один общий секрет на обоих концах – дёшево и быстро; это базовый вариант у Akamai EdgeAuth, Google Media CDN, в рецептах подписанных URL у Fastly, в Cloudflare Stream через signing key и у большинства CDN на NGINX. RSA использует пару ключей: вы подписываете приватным, CDN проверяет публичным. CloudFront и Mux идут по RSA-пути, потому что это позволяет менять «проверяющего» (публичный ключ в key group у CDN) независимо от «подписывающего» (бэкенда). Обе схемы дают непрозрачный blob, прикреплённый к URL; разница важна только при проектировании ротации ключей.

Рабочий пример. Подписываемый cleartext – каноническая форма запроса, который сделает плеер:

URL path:      /vod/customer42/manifest.m3u8
Expires:       1748185200            (Unix epoch seconds, ≈ 2025-05-25 13:00 UTC)
Allowed IP:    203.0.113.7/32
Customer:      customer42

Эта строка хэшируется секретом по SHA-256, результат base64-кодируется. CDN получает запрос, повторяет тот же хэш с тем же секретом и сравнивает. Один бит не совпал – неверный path, неверный expiry, IP вне диапазона – запрос отбрасывается прямо на edge.

Рис. 1. Четыре составляющих, общие для любой схемы подписанных URL.

Пять диалектов CDN рядом

Лексика разная, алгоритм один. Эту таблицу команды обычно хотели бы иметь в первый день.

CDNФормат токенаАлгоритмРазмещениеГде живёт спецификация
Amazon CloudFrontCanned или custom policy + signatureRSA-SHA1 (legacy) или RSA-SHA256 через key groupsQuery string (?Policy=…&Signature=…&Key-Pair-Id=…) или cookies (CloudFront-Policy, -Signature, -Key-Pair-Id)AWS CloudFront Developer Guide, разделы «Use signed URLs» и «Set signed cookies using a custom policy»
Akamai Adaptive Media Delivery (EdgeAuth)ACL + expiry + nonce + HMACHMAC-SHA256 (default)Query string, cookie или header – настраивается per-propertyAkamai TechDocs, «Enable Token Authentication»
Cloudflare StreamJWT (RS256)RSA-SHA256, генерируется через API token, signing key или Worker bindingКомпонент пути (/<token>/manifest.m3u8)Cloudflare Stream docs, «Secure your Stream»
Google Media CDNDual-token: короткий HMAC для path + длинная cookieHMAC-SHA1 / HMAC-SHA256 / Ed25519Query string + cookieGoogle Cloud «Edge cache origin → signed requests»
Mux (и большинство JWT-first платформ)Подписанный JWT, RS2562048-bit RSA key pairQuery string (?token=<JWT>)Mux «Secure video playback»

Три нюанса, которые потом портят жизнь:

Зарезервированные имена query-параметров различаются. CloudFront вырезает Key-Pair-Id, Policy и Signature перед форвардом на origin, но форвардит любое имя с суффиксом -PREFIX – это аварийный выход, если вашему приложению тоже нужен параметр с именем, например, Policy. У Akamai по умолчанию – hdnts; Google Media CDN использует edge-cache-token; Cloudflare Stream кладёт JWT именно в путь, а не в query string, чтобы URL оставался кэшируемым.

Ловушка IPv6. У CloudFront custom-policy с полем IpAddress – конструкция только для v4; если на дистрибутиве включён IPv6, IP-проверка тихо ломается. AWS это документирует; команды узнают об этом через три месяца, когда первый IPv6-only оператор приходит на iOS.

Алгоритм подписи мигрирует. Поток CloudFront с trusted key groups использует RSA-SHA256; старый поток trusted signers – RSA-SHA1 (поддерживается, но устаревает). HMAC-SHA1 у Akamai ещё в поле, но новые property нужно делать на HMAC-SHA256.

Подписанный URL или подписанная cookie – для стриминга оба

Для одного скачивания достаточно подписанного URL. Для HLS или DASH плеер скачает один master playlist, несколько media playlists, URI ключа, а потом сотни или тысячи сегментов. Можно переписать каждую URL в манифестах подписью per-request, можно один раз поставить подписанную cookie в начале сессии и дать плееру забрать всё внутри защищённого пути.

CloudFront в своей документации прямо говорит: подписанные cookies – правильный выбор, когда защищается «много файлов» или важно сохранить чистые, кэшируемые URL. Akamai с session token делает то же самое: access token валидирует сессию, потом «long» session token едет в cookie или query string весь остаток стрима. Компромисс: cookies требуют поддержки third-party cookies, что ломается на некоторых браузерах Smart TV и на iOS Safari при кросс-доменном плеере. Гибридный паттерн – подписанная cookie на манифесты и ключи плюс подписанный URL на master для bootstrap сессии – это де-факто прод-стандарт сегодня.

Если вы подписываете URL сегментов индивидуально, подписывайте общий префикс, а не полную URL каждого сегмента. CloudFront и Akamai поддерживают это через wildcard или «ACL» – одна подпись покрывает /customer42/title-9000/* на час, плеер не дёргает свежую подпись на каждый сегмент, cache key остаётся стабильным. Подпись каждой сегментной URL – вторая по частоте причина обвала cache hit ratio после криво настроенных cache keys.

Рис. 2. Два потока side-by-side для одной сессии воспроизведения HLS.

Истечение токена и проблема mid-session refresh

Токены короткоживущие специально: утёкшая URL должна перестать работать за минуты, а не за дни. Для VOD длительностью 15 минут expiry в 30 минут – нормально. Для трёхчасового концерта или полуторачасового фильма токен закончится посреди воспроизведения, и следующий запрос плейлиста или сегмента получит 403.

Есть три надёжных паттерна. Достаточно длинный expiry – самый простой: ставим срок, который точно переживёт самый длинный сеанс, мирясь с чуть большим окном для misuse. Token renewal – короткий expiry плюс обновление подписанной URL с бэкенда до её истечения; в hls.js это делается через слушателя события MANIFEST_LOADED с последующим hls.loadSource() на новой URL или через xhrSetup, который дописывает свежий токен в каждом запросе. Path-only signature – подписывается только базовый путь, а отдельный короткоживущий «session token» в cookie держит временное ограничение; подпись на пути не меняется на весь тайтл.

Особо стоит назвать переключение битрейта. Баг hls.js №1450 с Akamai, всё ещё цитируемый в комьюнити, – это ровно эта история: Akamai-токен подписывает /master.m3u8 и четыре variant playlists, на которые мастер ссылается; когда плеер впервые переключается вверх на rendition 5 Мбит/с, он запрашивает пятый variant playlist, чью URL никто не открывал и токен на который мог уже истечь. Лечится подписью общего ACL (/title-9000/*) вместо отдельных плейлистов и обновлением токена до конца самой длинной ожидаемой сессии.

Защита origin – вторая половина работы

Подписанный URL на CDN ничего не даст, если любопытный пользователь может постучаться прямо на origin. Замок собирается слоями.

Origin видит только трафик CDN. CloudFront делает это через Origin Access Control (OAC, современная замена OAI) для S3 или через подписанные origin-заголовки на custom HTTP origin. У Akamai – Site Shield с публичным списком IP, который пускает firewall origin. У Cloudflare – Authenticated Origin Pulls с клиентским сертификатом; у Google Cloud CDN – Cloud Armor плюс shared secret в X-Edge-Cache-Token. Принцип общий: network ACL origin отвергает всё, что пришло не с edge CDN.

Mutual TLS между CDN и origin – апгрейд для платного трафика. CDN предъявляет клиентский сертификат, origin его валидирует, посредники не могут притвориться CDN. AWS, Akamai, Fastly и Cloudflare поддерживают это на enterprise-тарифах.

Authenticated origin headers. CDN добавляет заголовок – CloudFront-Key-Pair-Id, custom HMAC или JWT – который origin проверяет на каждом запросе. Ремень и подтяжки: даже если кто-то соскрейпил IP origin, запросы без заголовка дропаются.

Обратный вопрос – «а если скомпрометирован сам CDN?» – это то, для чего существуют DRM и forensic watermarking. Это другой слой; статья заканчивается на сегменте, который покидает CDN.

Рис. 3. Defence in depth от edge-токена до firewall origin.

Ловушка cache key (или как подписанный URL тихо уничтожает hit ratio)

Это самый частый production-фейл с подписанными URL – и он стоит реальных денег.

CDN кэширует объект под cache key – детерминированной функцией от URL и подмножества заголовков и query-параметров. Если подпись попадает в cache key, каждый зритель получает свой уникальный cache key на тот же объект, hit ratio падает с 99% примерно до нуля, счёт за origin egress взрывается, и команда узнаёт об этом из финансового дашборда, а не из инженерного.

Три правила спасают. Уберите параметры подписи из cache key. CloudFront по умолчанию делает это для Policy, Signature и Key-Pair-Id; Akamai исключает hdnts; для любого другого вендора или произвольного имени параметра – проверьте и сконфигурируйте. Для общих ассетов используйте подписанные cookies, потому что cookies в default cache key не входят. Подписывайте общий путь, а не уникальную URL – cache key между зрителями не различается, потому что URL та же.

Вторая ловушка внутри этой: TTL кэша больше срока действия токена. CloudFront спокойно отдаст закэшированный 200 после того, как подпись в исходной URL истекла, потому что cache key не учитывает expiry. Для платного контента ставьте max-age на CDN короче, чем срок токена, или используйте cookie-сессии, где временной лимит несёт сама cookie.

Типичные ошибки

«Pitfall: подписывать каждый URL сегмента отдельно. Hit ratio обваливается, счёт за CDN утраивается. Подписывайте общий ACL или используйте signed cookie на уровне сессии.»

Ещё три типичных провала. Хранить ключ подписи во фронтенде (он появляется в view-source). Ставить одинаковый expiry для всех (одна успешная replay-атака работает всё окно – привяжите к IP, user ID или device). Пропускать шаг защиты origin, потому что «signed URL и так достаточно» (нет – без origin ACL URL источника находится одним whois-запросом).

Где здесь Фора Софт

Большинство наших стриминговых, OTT, телемедицинских и e-learning проектов начинается с одного и того же разговора: пайплайн видео уже работает, и его нужно поставить за paywall, региональную лицензию или список доступа «только клиницисты». Мы проектируем поток signed URL под выбранный заказчиком CDN, настраиваем lockdown origin (OAC, Site Shield, Authenticated Origin Pulls или собственный edge-токен), инструментируем плеер так, чтобы события истечения токена становились алертами, а не жалобами зрителей. Тот же каркас лежит и в наших проектах видеоконференцсвязи и видеонаблюдения, где список доступа – per-room или per-camera, а не per-asset.

Ключевые выводы

  • Signed URL и signed cookie используют одну математику; для HLS/DASH cookie удобнее.
  • HMAC и RSA отличаются ротацией ключей, а не уровнем защиты.
  • В cache key не должна попадать подпись – иначе экономика CDN заканчивается.
  • Подписывайте общий путь (ACL или wildcard), а не каждый URL сегмента.
  • Защита origin – OAC, Site Shield, mTLS или authenticated header – обязательна.
  • Планируйте expiry под самую длинную сессию и обновляйте токен mid-stream.

Что читать дальше

Призыв к действию

Обсудите с инженером по стримингу проектирование signed URL и защиты origin для вашей платформы · Посмотрите наши кейсы в стриминге, OTT и телемедицине · Скачайте чек-лист hardening для signed URL – одна страница с 12 пунктами конфигурации, которые нужно проверить до запуска платного стриминга.

Строите такую систему?

Подберём параметры кодирования под ваш контент и посчитаем стоимость доставки до старта разработки.