Авторизация просмотра
Настройка всех пяти режимов auth_backend с примерами, передача токена плееру, поведение по протоколам и типичные ошибки интеграции.
Авторизация просмотра закрывает всю медиа-выдачу: HLS, LL-HLS, DASH, MSE-WS, HTTP-TS, WHEP, RTSP, выдачу по RTMP, превью-кадры, DVR, таймшифт, экспорт клипов и видео по запросу. Она же закрывает приём публикации — см. Авторизация публикации.
Перед чтением этой страницы имеет смысл разобрать Модель доступа: здесь предполагается, что вы знаете разницу между грантом и билетом.
Общая форма#
Режим задаётся единственным блоком — на сервер один бэкенд:
auth_backend { type hs256; secret "секрет-подписи-токенов-зрителей"; }
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
type |
none · internal · hs256 · jwks · http |
пусто | горячее |
url |
строка; обязателен для jwks и http |
пусто | горячее |
secret |
строка; обязателен для hs256 |
пусто | горячее |
issuer |
строка; проверяется, только если задан | пусто | горячее |
audience |
строка; проверяется, только если задан | пусто | горячее |
perms_claim |
строка — имя claim'а с правами | grants |
горячее |
revoked_url |
строка; имеет смысл только для jwks |
пусто | горячее |
tls_verify |
insecure — не проверять цепочку TLS у источника ключей |
пусто (проверять) | горячее |
Ошибки проверки конфигурации приводятся дословно:
auth_backend.type must be one of none|internal|hs256|jwks|http (got "x") auth_backend type "jwks" requires a url auth_backend type "hs256" requires a secret auth_backend type "internal" needs at least one `principal` block
Весь блок применяется горячо: следующее установление сессии пойдёт через новый бэкенд, уже
установленные сессии доигрывают по своим билетам. Проверьте файл до применения:
hastreamer --validate /etc/hastreamer/hastreamer.conf.
Режим none — блока нет#
# либо явно: auth_backend { type none; }
Медиа открыто всем. Никаких предупреждений при старте про открытые потоки не печатается — проверяйте это аудитом конфигурации.
Режим internal — офлайн-список принципалов#
Подходит, когда потребителей единицы и они постоянные: витрина в холле, стена мониторинга, служебная запись на соседний сервер.
auth_backend { type internal; } principal alice { pass hunter2; # либо sha256:<hex> permission read cam hls; # действие · путь · протоколы } principal svc { token s3cr3t-service-token-value; # непрозрачный токен permission read cam; permission playback cam; }
Проверено на стенде:
# токен принимается и строкой запроса, и заголовком curl -s -o /dev/null -w '%{http_code}\n' \ "http://media.example.com:8080/cam/index.m3u8?token=s3cr3t-service-token-value" curl -s -o /dev/null -w '%{http_code}\n' \ -H 'Authorization: Bearer s3cr3t-service-token-value' \ http://media.example.com:8080/cam/index.m3u8 # пара логин/пароль — обычная Basic-аутентификация curl -s -o /dev/null -w '%{http_code}\n' -u alice:hunter2 \ http://media.example.com:8080/cam/index.m3u8 # без учётных данных curl -s -o /dev/null -w '%{http_code}\n' http://media.example.com:8080/cam/index.m3u8
200 200 200 401
Пароль можно хранить хешем — сервер сравнивает его константным временем:
printf '%s' 'hunter2' | sha256sum | cut -d' ' -f1
f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7
principal alice { pass sha256:f52fbd32b2b3b86ff88ef6c490628285f482af15ddcb29541f94bcf526a3f6c7; }
Полный синтаксис permission и ограничения — Поля токена.
Режим hs256 — JWT на общем секрете#
Основной рабочий режим для временных ссылок: ваш бэкенд подписывает JWT тем же секретом, что задан на сервере, и подставляет его в URL плеера.
auth_backend { type hs256; secret "очень-длинный-общий-секрет"; issuer "https://idp.example"; # необязательно audience "sirinvideo"; # необязательно perms_claim "grants"; # по умолчанию "grants" }
Полезная нагрузка минимального токена:
{"sub":"viewer-42","exp":1787200000, "grants":[{"action":"read","path":"cam","protocols":["hls"]}]}
Алгоритм жёстко закреплён: заголовок обязан содержать ровно HS256. Попытка alg: none →
401 (проверено).
Готовый скрипт выпуска — Генераторы токенов.
Режим jwks — асимметричные токены внешнего поставщика#
Подходит, когда токены выпускает уже существующая служба входа и делиться с медиасервером секретом подписи нельзя.
auth_backend { type jwks; url https://idp.example/.well-known/jwks.json; issuer "https://idp.example"; audience "sirinvideo"; perms_claim "grants"; # revoked_url https://idp.example/revoked; # список отозванных jti # tls_verify insecure; # только для самоподписанного сертификата у источника ключей }
Поддерживаются RS256 (RSA + SHA-256) и ES256 (ECDSA P-256). Алгоритм привязан к типу ключа: RS256 проверяется только ключом RSA, ES256 — только EC.
Ключи тянет фоновая задача:
| Событие | Поведение |
|---|---|
| успешная загрузка | повтор через 600 секунд |
| ошибка загрузки | повтор через 30 секунд; последние удачные ключи не стираются |
неизвестный kid в токене |
досрочное обновление, не чаще одного раза в 15 секунд |
| размер тела | не более 1 МиБ, таймаут 10 секунд |
| кэш ключей пуст | отказ всем токенам (fail-closed) |
Пока набор ключей не загружен, любой токен получает отказ. В журнале это видно так:
[authz] auth_backend type "jwks": no keys cached (fetch pending/failed) — denying (fail-closed)
Строка печатается не более пяти раз, чтобы не заспамить журнал. Признак успеха — обратная строка (в журнал попадает только адрес источника без пути — ключи и параметры туда не утекают):
[authio] jwks: loaded 2 key(s) from https://idp.example
Неудачная попытка обновления выглядит так и не стирает уже загруженные ключи:
[authio] jwks: fetch failed (https://idp.example): <текст ошибки> — keeping the last-good set, retrying
Режим http — ваша внешняя служба#
Сервер обращается к вашему адресу методом POST на каждое установление сессии (не на сегмент —
за это отвечает билет).
auth_backend { type http; url https://auth.example/authorize; }
Тело запроса — снятый со стенда payload:
{"ip":"203.0.113.7","user":"","password":"","token":"allow-all","action":"read", "privilege":"live","path":"cam","labels":["site-north"],"protocol":"hls", "query":"token=allow-all","user_agent":"curl/8.18.0"}
Поле privilege уточняет действие до конкретной поверхности: live, dvr, export, preview
для просмотра и publish для приёма публикации.
Интерпретация вашего ответа:
| Ответ службы | Что видит клиент | Смысл |
|---|---|---|
2xx с пустым телом |
200 |
разрешить ровно запрошенную пару действие + путь |
2xx + {"sub":…,"ttl_secs":…} |
200 |
то же плюс субъект учёта и срок билета |
2xx + {"permissions":[…]} |
200 |
заменить набор прав |
401 |
401 |
учётные данные отвергнуты |
403 |
403 |
авторизация отвергнута |
408, 425, 429, 5xx |
503 |
служба недоступна → отказ (fail-closed) |
| прочее либо битый JSON | 502 |
нарушен контракт → отказ |
Ограничения: тело ответа не более 64 КиБ, таймаут 5 секунд (не настраивается), не более 256 одновременных вызовов на ядро.
Пример минимальной службы на стандартной библиотеке Python — Генераторы токенов.
Исключения: public_paths#
public_paths health lobby/*;
| Параметр | Тип и допустимые значения | По умолчанию | Применение |
|---|---|---|---|
public_paths |
список литералов и *-глобов через пробел; повторное указание добавляет к списку |
[] |
горячее |
Путь из списка проверяется раньше любого бэкенда и полностью выводится из-под авторизации.
Важный побочный эффект: только public_paths сохраняет кэшируемость сегментов (см. ниже).
Как отдать токен плееру#
Три рабочих способа, по убыванию удобства:
- Строка запроса на плейлисте. Ваш бэкенд отдаёт готовую ссылку:
http://media.example.com:8080/cam/index.m3u8?token=$TOKEN. Дальше плеер идёт по URI из плейлиста, в которые сервер уже вплёл?ssid=. Это единственный способ, работающий для MSE-WS в браузере: заголовки на WebSocket задать нельзя. - Заголовок
Authorization: Bearer. Годится для нативных приложений и серверных интеграций, где заголовками управляете вы. Basic-аутентификация — только для режимовinternalиhttp.
Самая частая ошибка интеграции — выдать зрителю прямую ссылку на сегмент с ?token=. Проверено:
сегмент с абсолютно валидным токеном возвращает 401, потому что сегментный гейт проверяет
исключительно билет ?ssid=:
curl -s -o /dev/null -w '%{http_code}\n' \ "http://media.example.com:8080/cam/v0/1787143547.m4s?token=$TOKEN" curl -s -o /dev/null -w '%{http_code}\n' \ "http://media.example.com:8080/cam/v0/1787143547.m4s?ssid=<ssid-из-плейлиста>"
401 200
Поведение по протоколам#
| Протокол | Где предъявляется грант | Действие | Привилегия | Значение в protocols |
|---|---|---|---|---|
| HLS, LL-HLS | master.m3u8, index.m3u8, index.ll.m3u8 |
read |
live |
hls |
| DASH | index.mpd |
read |
live |
dash |
| MSE-WS | апгрейд GET /{поток}/mse.ws |
read |
live |
mse-ws |
| HTTP-MPEG-TS | GET /{поток}/stream.ts |
read |
live |
http-ts |
| WHEP (WebRTC) | POST /s/{поток}/whep |
read |
live |
whep |
| RTSP | DESCRIBE |
read |
live |
rtsp |
| RTMP-выдача | команда play |
read |
live |
rtmp |
| Превью-кадр | GET /{поток}/preview.mp4 |
read |
preview |
hls |
| DVR, таймшифт | dvr/**, rewind-*.m3u8, timeshift-*.m3u8 |
playback |
dvr |
hls |
| Экспорт клипа | dvr/clip?from&to, clip-*.mp4 |
playback |
export |
hls |
| Видео по запросу | /vod/{библиотека}/{ассет}/… |
playback |
dvr |
hls |
Сегменты, init-объекты и части всех перечисленных HTTP-протоколов проверяются только билетом.
Что стоит знать по отдельным протоколам#
- HLS и DASH делят одни и те же объекты сегментов, но билеты у них разные: билет, выпущенный
на
index.m3u8, не откроет сегмент, запрошенный как DASH, и наоборот. - MSE-WS: браузер не может задать заголовок на WebSocket, поэтому на практике остаётся
?token=. Есть отдельная пробаHEAD /{поток}/mse.ws— она отвечает204,401или403и позволяет проверить право до открытия сокета. - RTSP: грант проверяется один раз, на первом запросе с URI (
DESCRIBE). Учётные данные принимаются и заголовкомAuthorization(Basic или Bearer), и как?token=прямо в RTSP-URI.SETUPиPLAYпосле допуска идут по установленной сессии. - RTMP-выдача: заголовков нет, поэтому учётные данные едут только в строке запроса URL —
rtmp://media.example.com/live/cam?token=$TOKEN. Билетов RTMP не использует: одно соединение, авторизуемое один раз. - Превью-кадр требует отдельной привилегии
previewи не выпускает билет — это одиночный объект. - Экспорт клипа требует привилегии
export. Обычное разрешение сaction: playbackи подходящим путём открывает и архив, и экспорт; разрезать их можно только компактным грантом по метке — см. Метки потоков.
Токен с "protocols":["hls"] даёт 403 на mse.ws, index.mpd и RTSP (проверено). Если ваш
плеер умеет переключать транспорт при потере связи, перечислите все протоколы, которые он может
попробовать, — или не указывайте protocols вовсе.
Грабли#
Билет привязан к User-Agent#
Смена User-Agent внутри одной сессии — 403. Это ловит три реальных сценария: «скачаю ссылку из
браузера через curl», прокси, переписывающий заголовок, и клиент, обновившийся посреди сессии.
UA='Mozilla/5.0 MyPlayer' curl -s -o /dev/null -w '%{http_code}\n' -A "$UA" "http://media.example.com:8080/cam/v0/$SEG" curl -s -o /dev/null -w '%{http_code}\n' -A "Other/1.0" "http://media.example.com:8080/cam/v0/$SEG"
200 403
Тот же эффект даёт session_keys { bind_ip on; } при смене адреса — учитывайте это для мобильных
клиентов и сетей за CG-NAT.
Включение авторизации отключает кэширование сегментов#
Понижение до Cache-Control: no-store вызывает любой из двух факторов: у потока задан
allowed_referers, или включён auth_backend и путь не входит в public_paths.
Проверено на одном и том же объекте:
# сервер без auth_backend curl -sI "http://media.example.com:8080/cam/v0/init.mp4" | grep -i cache-control # сервер с auth_backend curl -sI "http://media.example.com:8080/cam/v0/init.mp4?ssid=$SSID" | grep -i cache-control
Cache-Control: public, max-age=31536000, immutable Cache-Control: no-store
Понижение применяется одинаково на всех трёх ветках выдачи — живой, DVR и видео по запросу.
Каждый сегмент каждого зрителя будет доходить до сервера-источника: для крупной раздачи это
многократный рост исходящего трафика. Если нужен CDN — оставьте поток в public_paths и без
allowed_referers, а доступ ограничивайте средствами самого CDN. Планируйте это до запуска,
а не после.
principal any превращает все 401 в 403 по всему серверу#
Принципал с именем any совпадает с любым запросом, поэтому отказ всегда классифицируется как
«прав нет», а не «предъявите данные». Проверено:
| Запрос | Без principal any |
С principal any |
|---|---|---|
| без учётных данных | 401 + WWW-Authenticate: Bearer realm="hastreamer" |
403, заголовка WWW-Authenticate нет вовсе |
| неверный пароль | 401 |
403 |
| верные данные | 200 |
200 |
Клиенты, которые ждут 401, чтобы предложить ввести пароль, ломаются молча — плеер просто покажет
ошибку.
public_paths, а не principal anypublic_paths снимает проверку раньше бэкенда, не трогает коды ответов на остальных путях и
сохраняет кэшируемость сегментов. principal any не делает ничего из этого.
За обратным прокси обязателен trusted_proxies#
Заголовки X-Forwarded-For, Forwarded и X-Real-IP читаются, только если непосредственный
клиент перечислен в trusted_proxies. Без этого привязка по IP — и в токене, и в билете —
закрепится на адресе прокси: либо всё сломается, либо «привязка» перестанет что-либо значить.
Троттлинг бьёт по всем за общим NAT#
Восемь неудачных попыток за 60 секунд с одного адреса → блокировка с ответом 429 throttled и
заголовком Retry-After. Отказы вида 403 (подпись верна, прав нет) троттл не кормят
намеренно — иначе плеер, опрашивающий недоступный ему поток, заблокировал бы себе остальные.
caps — это не контроль доступа#
stream cam { caps hls mse-ws; } ограничивает только вкладки встроенного плеера. Поток с
caps hls; по-прежнему отдаётся по MSE-WS, DASH и RTSP.
Рецепты#
Закрыть все потоки и выдавать временные ссылки#
http 8080; auth { # иначе панель управления открыта всем admin_password "длинный-пароль-администратора"; session_secret "общий-секрет-32-и-более-символов"; } auth_backend { # закрываем просмотр type hs256; secret "секрет-подписи-токенов-зрителей"; } session_keys { ttl 3600; } # срок билета сессии stream cam { input rtsp://admin:пароль@10.0.0.10:554/Streaming/Channels/101; }
Ссылку зрителю выдаёт ваш бэкенд: подписывает JWT тем же секретом и отдаёт
http://media.example.com:8080/cam/index.m3u8?token=$TOKEN.
Токен на один поток на N минут#
{"sub":"viewer-42","exp":1787200000, "grants":[{"action":"read","path":"cam","protocols":["hls"]}]}
Ограничение по адресу зрителя#
{"exp":1787200000,"ip":"203.0.113.7", "grants":[{"action":"read","path":"cam","protocols":["hls"]}]}
{"exp":1787200000,"net":["203.0.113.0/24","2001:db8::/32"], "grants":[{"action":"read","path":"cam","protocols":["hls"]}]}
Оба варианта проверены: попадание → 200, промах → 403. Третий, независимый механизм —
session_keys { bind_ip on; }: он привязывает к адресу сам билет, то есть и все сегменты.
Публичный поток-исключение#
public_paths lobby/*; stream lobby/hall { input rtsp://10.0.0.10:554/live; }
Проверка установки#
# 1. плейлист без токена curl -s -o /dev/null -w '%{http_code}\n' http://media.example.com:8080/cam/index.m3u8 # 2. плейлист с токеном curl -s -o /dev/null -w '%{http_code}\n' "http://media.example.com:8080/cam/index.m3u8?token=$TOKEN" # 3. заголовок кэширования на init-объекте curl -sI "http://media.example.com:8080/cam/v0/init.mp4?ssid=$SSID" | grep -i cache-control
401 200 Cache-Control: no-store
Если шаг 1 вернул 200 — блок auth_backend не применился либо путь попал в public_paths.
Если шаг 1 вернул 403 — почти наверняка в конфигурации есть principal any.