SIRINVIDEO Документация администратора и интегратора На сайт 2026.08

Авторизация просмотра

Настройка всех пяти режимов 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: none401 (проверено).

Готовый скрипт выпуска — Генераторы токенов.

Режим 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 сохраняет кэшируемость сегментов (см. ниже).

Как отдать токен плееру#

Три рабочих способа, по убыванию удобства:

  1. Строка запроса на плейлисте. Ваш бэкенд отдаёт готовую ссылку: http://media.example.com:8080/cam/index.m3u8?token=$TOKEN. Дальше плеер идёт по URI из плейлиста, в которые сервер уже вплёл ?ssid=. Это единственный способ, работающий для MSE-WS в браузере: заголовки на WebSocket задать нельзя.
  2. Заголовок Authorization: Bearer. Годится для нативных приложений и серверных интеграций, где заголовками управляете вы.
  3. 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

Каждый сегмент каждого зрителя будет доходить до сервера-источника: для крупной раздачи это многократный рост исходящего трафика. Если нужен 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 any

public_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.