PAYATHON 2026

LibVLC 与 ASP.NET 授权 HLS 流的 Cookie 转发问题

排障赵师傅

结论

--http-forward-cookies 无法可靠解决这个问题。它主要用于在 HTTP 请求链中转发 Cookie,例如处理重定向,但不保证 LibVLC 3 的 HLS adaptive 模块会保存 master playlist 响应中的 Set-Cookie,并把 Cookie 带到子 playlist、初始化文件和媒体分片请求中。

从日志可以看出问题所在:

set-cookie: "stream_session=...; path=/; samesite=lax; httponly"
...
Retrieving https://localhost:7002/.../audio_64k.m3u8
...
:status: "401"

第一条日志只能证明 LibVLC 收到了 Set-Cookie 响应头,无法证明 Cookie 已保存到后续 HLS 请求使用的 Cookie 容器中。子 playlist 由 adaptive 模块单独请求。该请求没有携带 Cookie,服务器因此返回 401。

生产环境建议使用有效期较短、限制资源范围的流访问令牌,并把令牌写入每个 HLS 资源 URI。也可以通过受控代理统一添加认证信息。播放授权不应依赖 LibVLC 自动接收和转发 Cookie。

为什么当前配置不起作用

一次 HLS 播放会发起多次 HTTP 请求,可能涉及:

  1. master playlist;
  2. variant/media playlist;
  3. EXT-X-MAP 指定的初始化文件;
  4. 音视频分片;
  5. 加密密钥或字幕等附加资源。

master playlist 中的相对地址,例如:

audio_64k.m3u8

会被解析为:

https://localhost:7002/api/stream/{trackId}/audio_64k.m3u8

master URL 的查询参数不会自动传给子请求。因此:

master.m3u8?session=abc

对应的子请求仍然是原始地址,并不会自动变成:

audio_64k.m3u8?session=abc

--http-forward-cookies 也不会把收到的所有 Cookie 自动附加到全部 HLS 子请求。即使同时把它配置为 LibVLC 实例参数和 media option,这一限制仍然存在。

日志里的 206 Partial Content 来自 enableRangeProcessing: true,表明 master playlist 的 Range 请求已经成功返回,并非故障。

推荐方案:让每个 HLS URI 都携带令牌

客户端先通过受认证的 API 获取短期流令牌。令牌应当:

  • 使用不可预测的随机值或带签名的值;
  • 绑定到指定 track_id;
  • 设置较短的有效期;
  • 可选地绑定用户、设备或播放会话;
  • 支持撤销,并限制使用范围。

客户端只需把令牌放在 master playlist URL 中:

var session = await m_StreamService.CreateSession(track);

var url =
    $"https://my-server/hls/{track}/master.m3u8" +
    $"?session={Uri.EscapeDataString(session.ToString())}";

using var media = new Media(m_LibVLC, url, FromType.FromLocation);

media.AddOption(":network-caching=1000");
media.AddOption(":live-caching=1000");
media.AddOption(":http-reconnect");

m_MediaPlayer.Media = media;

服务器返回 master playlist 时,把令牌追加到每个子 playlist URI:

#EXTM3U
#EXT-X-STREAM-INF:BANDWIDTH=64000,CODECS="mp4a.40.2"
audio_64k.m3u8?session=8dd048ec-1906-44f3-b615-9c0c820c27ef
#EXT-X-STREAM-INF:BANDWIDTH=128000,CODECS="mp4a.40.2"
audio_128k.m3u8?session=8dd048ec-1906-44f3-b615-9c0c820c27ef

media playlist 中的初始化文件、分片和密钥等 URI 也需要携带令牌:

#EXTM3U
#EXT-X-MAP:URI="init_audio.mp4?session=8dd048ec-1906-44f3-b615-9c0c820c27ef"
#EXTINF:6.000,
segment_0001.m4s?session=8dd048ec-1906-44f3-b615-9c0c820c27ef
#EXTINF:6.000,
segment_0002.m4s?session=8dd048ec-1906-44f3-b615-9c0c820c27ef

如果使用 EXT-X-KEY、EXT-X-MEDIA 或字幕 playlist,也要改写相应的 URI。

ASP.NET 端的实现方式

所有 HLS 资源可以共用一套授权检查:

private bool TryValidateStreamSession(
    Guid trackId,
    Guid session,
    out IActionResult? error)
{
    if (session == Guid.Empty)
    {
        error = Unauthorized();
        return false;
    }

    if (!m_StreamingService.ValidateSession(session, trackId))
    {
        error = Unauthorized();
        return false;
    }

    error = null;
    return true;
}

子 playlist 和分片接口直接读取查询参数:

[HttpGet("{track_id}/{playlist}.m3u8")]
public IActionResult GetAudio(
    [FromRoute] Guid track_id,
    [FromRoute] string playlist,
    [FromQuery] Guid session)
{
    if (!TryValidateStreamSession(track_id, session, out var error))
        return error!;

    // 校验 playlist,读取文件并改写其中的所有资源 URI。
    // 返回 Content-Type: application/vnd.apple.mpegurl
}

[HttpGet("{track_id}/{segment}.m4s")]
public IActionResult GetSegment(
    [FromRoute] Guid track_id,
    [FromRoute] string segment,
    [FromQuery] Guid session)
{
    if (!TryValidateStreamSession(track_id, session, out var error))
        return error!;

    // 校验路径并返回分片。
}

不要对 playlist 文本直接做全局字符串替换。以下内容需要分别处理:

  • 非注释行中的资源地址;
  • EXT-X-MAP 的 URI;
  • EXT-X-KEY 的 URI;
  • EXT-X-MEDIA 的 URI;
  • 已有查询参数的 URI;
  • 绝对 URI 和相对 URI。

URI 已有查询参数时使用 &session=...,没有查询参数时使用 ?session=...。令牌值还需要经过 URL 编码。

部分 LibVLC 构建或访问模块可能支持类似下面的 media option:

media.AddOption($":http-cookie=stream_session={session}");

这个选项可以用于兼容性测试,但不能默认它会传递到 LibVLC 3 adaptive 模块创建的所有 HLS 请求。不同 LibVLC 版本、平台和网络访问模块的行为可能不同,需要实际检查子 playlist、分片和 EXT-X-MAP 请求。

即使初始请求携带了 Cookie,也要逐项验证:

  • 子 playlist 是否携带 Cookie;
  • 分片和初始化文件是否携带 Cookie;
  • playlist 更新、码率切换和重连后是否继续携带;
  • 跳转到其他主机或 CDN 后是否仍符合预期。

未经验证的 :http-cookie 或自定义 header option 不适合作为唯一的访问控制措施。

另一种可靠方案:本地认证代理

如果不希望令牌出现在每个上游 URL 中,可以在 Avalonia 客户端内运行一个只监听回环地址的 HTTP 代理:

LibVLC
  -> http://127.0.0.1:{randomPort}/stream/master.m3u8
  -> Avalonia 本地代理
  -> ASP.NET HLS 服务

本地代理负责:

  • 使用 HttpClient 请求上游资源;
  • 在每个请求中添加 Authorization 或 Cookie;
  • 把 playlist 内的 URI 改写为本地代理地址;
  • 将 Range、状态码、Content-Type 和响应体转发给 LibVLC。

这样可以完全控制认证头,避开 LibVLC 的 Cookie 行为差异。实现时仍要正确处理 Range 请求、并发、取消、缓存、playlist 刷新和 URI 改写。

安全注意事项

查询参数会出现在 URL 中,需要采取以下措施:

  • 全程使用 HTTPS;
  • 不要把完整查询字符串写入访问日志;
  • 设置较短的令牌有效期;
  • 将令牌绑定到 track_id,避免跨资源复用;
  • 播放结束后尽可能撤销会话;
  • 不要用可预测的 GUID 或用户 ID 直接充当授权凭据;
  • 避免令牌通过重定向、错误页面或外部资源泄露;
  • 明确缓存策略,防止授权 playlist 进入共享缓存。

例如,动态 playlist 可以返回:

Response.Headers.CacheControl = "private, no-store";

媒体分片是否允许缓存,需要根据 CDN 和令牌校验方式单独设计。如果 URL 包含令牌,还要确认缓存键包含查询参数。

如果继续保留 Cookie 方案,至少应明确设置:

Response.Cookies.Append(
    "stream_session",
    session.ToString(),
    new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.Lax,
        Path = $"/api/stream/{track_id}/"
    });

这些属性可以限制 Cookie 的使用范围,却不能促使 LibVLC 的 HLS 模块自动共享 Cookie。

如何选择

对现有架构改动最小的做法,是继续使用当前 session 服务,同时取消子请求对 Cookie 的依赖。服务器动态改写 master playlist 和 media playlist,让每个 HLS URI 都携带短期 session。

如果访问令牌不能出现在 URL 中,可以使用本地代理,或者使用服务端/CDN 提供的签名 URL、签名 Cookie 机制。采用签名 Cookie 前,仍需确认当前 LibVLC 版本会在所有 HLS 子请求中发送 Cookie。

备注:内容仅供参考。