> ## Documentation Index
> Fetch the complete documentation index at: https://kawax.biz/llms.txt
> Use this file to discover all available pages before exploring further.

# Mercure 广播驱动内部机制

> 基于源码解读 Laravel framework 新增的 Mercure 广播驱动：与 Reverb、Pusher 不同的 HTTP/SSE 架构、授权 Cookie，以及端到端加密频道。

<Warning>
  本页介绍的 `MercureBroadcaster` 由 [laravel/framework PR #61474](https://github.com/laravel/framework/pull/61474)（2026 年 9 月 10 日合并）引入。截至撰写时，该功能尚未包含在任何标签发布中，[官方文档](https://laravel.com/docs/broadcasting)也未收录。本文是基于源码和 docblock 的前瞻性解读，实际使用前请查阅 `laravel/framework` 的变更记录。
</Warning>

## 概述

长期以来，Laravel 的广播层提供了 [Reverb](/zh-CN/broadcasting)、Pusher、Ably 这类"WebSocket 原生"驱动，而现在 `config/broadcasting.php` 的 `driver` 中新增了 `mercure`。

[Mercure](https://mercure.rocks) 是一种基于 Server-Sent Events (SSE) 的实时通信协议。它并非使用 WebSocket 那样的双向连接，而是运行在类似 HTTP/1.1 或 HTTP/2 长轮询之上，因此具有以下特点：

* 可以透明地穿越常见的 HTTP 基础设施（反向代理、CDN、负载均衡器）
* 客户端只需浏览器内置的 `EventSource` API 即可实现，无需专门的客户端库
* [FrankenPHP](/zh-CN/blog/laravel-cloud) 内置了 Mercure hub，无需额外基础设施即可运行

```mermaid theme={null}
flowchart LR
    A["服务端<br>broadcast(event)"] --> B["MercureBroadcaster"]
    B --> C["Mercure hub<br>(Hub / FrankenPhpHub)"]
    C -->|"SSE (EventSource)"| D["浏览器<br>Laravel Echo"]
    D --> E["UI 实时更新"]
```

## 新增的 `driver` 值

`config/broadcasting.php` 顶部的支持驱动注释列表中新增了 `mercure`：

```php theme={null}
// Supported: "reverb", "pusher", "ably", "mercure", "redis", "log", "null"
```

同时还提供了连接配置示例：

```php theme={null}
'mercure' => [
    'driver' => 'mercure',
    'url' => env('MERCURE_URL'),
    'public_url' => env('MERCURE_PUBLIC_URL'),
    'secret' => env('MERCURE_JWT_SECRET'),
    'encryption_key' => env('MERCURE_ENCRYPTION_KEY'),
    'claims' => [
        'iss' => env('MERCURE_JWT_ISSUER'),
        'client_id' => env('APP_NAME'),
    ],
    'cookie_name' => env('MERCURE_COOKIE_NAME'),
    'subscribe_expiration' => (int) env('MERCURE_SUBSCRIBE_EXPIRATION', 5),
],
```

若省略 `url`，驱动会自动回退到 FrankenPHP 内置的 Mercure hub（即 `mercure_publish()` 函数）。该判断由 `CreatesMercureDrivers::mercure()` 完成：

```php theme={null}
public function mercure(array $config)
{
    if (empty($config['url'])) {
        return $this->frankenPhpMercure($config);
    }

    // ...
}
```

当你运行外部 hub 时，`url` 应指向用于发布的管理 API，而 `public_url` 是浏览器实际连接的 URL。区分两者是为了支持这样的部署：发布走内部网络（如 Docker Compose），只把公开 URL 暴露给浏览器。

## 分离的发布与订阅令牌

Mercure 是一种以 JWT 进行访问控制的协议。`CreatesMercureDrivers` trait 会为发布（服务端 → hub）和订阅（浏览器 → hub）分别构建**独立的令牌工厂**：

* `secret`、`publish_secret`、`subscribe_secret` 可分别配置，缺省时回退到 `secret`
* `algorithm`、`publish_algorithm`、`subscribe_algorithm` 同样可按方向单独设置（默认 `HS256`）
* HS256 要求密钥至少 32 字节，HS384 至少 48 字节，HS512 至少 64 字节；否则会抛出 `InvalidArgumentException`

```php theme={null}
protected function mercureSecret(array $config, string $side)
{
    $secret = ($config[$side.'_secret'] ?? null) ?: ($config['secret'] ?? null);

    // ...

    $minimumLength = ['HS256' => 32, 'HS384' => 48, 'HS512' => 64][$algorithm] ?? 0;

    if (strlen($secret) < $minimumLength) {
        throw new InvalidArgumentException(/* ... */);
    }

    return $secret;
}
```

发布令牌对所有主题（`*`）持有 `Grant::ACTION_PUBLISH` 权限，并由 `CachingTokenProvider` 缓存，从而避免每次广播都重新生成 JWT。

## 通过单一 Cookie 完成频道授权

与 WebSocket 驱动最大的架构差异在于：订阅端的授权是通过**一个 Cookie** 完成的。`MercureBroadcaster::auth()` 接收一个 `channel_names` 数组（每次请求最多 100 项），对每个频道分别评估授权，然后统一签发一个覆盖所有频道的授权 Cookie：

```php theme={null}
public function auth($request)
{
    $channelNames = (array) $request->input('channel_names', []);

    if ($channelNames === [] ||
        count($channelNames) > 100 ||
        $channelNames !== array_filter($channelNames, 'is_string')) {
        throw new AccessDeniedHttpException;
    }

    // 对每个频道评估授权并累积 grant……

    return (new JsonResponse([/* ... */]))
        ->cookie($this->makeAuthorizationCookie($request, $grants, $user));
}
```

关键设计是：单个频道被拒绝并不会导致整个响应失败——被拒绝的频道会在响应载荷中标记为 `denied: true`，其余获授权的频道仍继续工作。这一点很重要，因为 Mercure 会在一条 EventSource 连接上多路复用许多主题，中途增加或移除频道时无需断开连接。

Presence 频道使用的 `Grant` 与普通 `private`/`private-encrypted` 频道不同，它针对的是 Mercure [Subscription API](https://mercure.rocks/docs/hub/concepts/active-subscriptions) 所使用的订阅 URL 模式。

## 端到端加密频道

以 `private-encrypted-` 为前缀的频道被视为端到端加密（E2EE）——这意味着 Mercure hub 本身也看不到消息负载。这是现有 Pusher 与 Reverb 驱动都不具备的能力。

将 `encryption_key` 配置为 base64 编码的 32 字节密钥后，`ChannelEncrypter` 便会启用；在 `broadcast()` 发送之前，它会把事件名、载荷和 socket ID 一并封装为 JWE（JSON Web Encryption）。hub 中转的只是已加密的字节流。

```php theme={null}
'encryption_key' => env('MERCURE_ENCRYPTION_KEY'),
```

```shell theme={null}
php -r "echo base64_encode(random_bytes(32));"
```

订阅端在浏览器中使用 `auth()` 响应返回的 `jwk`（JSON Web Key）字段进行解密——这是 Mercure 规范推荐的带外密钥交换方式，hub 始终无从得知密钥。

<Info>
  Presence 频道无法加密，因为成员列表本身就是通过 hub 的 Subscription API 流转的——这与 E2EE 在设计上无法共存。
</Info>

## Whisper（客户端之间的直接消息）

当 `client_events` 启用（默认开启）时，每个受保护的频道都会分配一个专用的"whisper 主题"，订阅者可以向该主题发布消息：

```php theme={null}
if ($this->clientEvents && $whisperTopics !== []) {
    $grants[] = new Grant([Grant::ACTION_SUBSCRIBE, Grant::ACTION_PUBLISH], $whisperTopics);
}
```

频道自身的主题保持仅服务端可写，因此客户端永远无法伪造来自服务端的事件。这一设计与 Pusher 的"client events"功能类似，但通过区分主题让权限划分更加清晰。

## 主题命名

由于 Mercure hub 常常在多个应用之间共享，主题需要在 `topic_prefix` 命名空间下进行隔离以避免冲突。默认值为：

```php theme={null}
protected string $topicPrefix = 'https://laravel.alt/echo/',
```

`.alt` 是保留的、不可解析的 DNS 后缀（[RFC 9476](https://www.rfc-editor.org/rfc/rfc9476.html)），因此绝不会与真实域名冲突。频道名会按照 [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986.html) 进行 URL 编码，并置于单个路径段中：

```php theme={null}
protected function channelTopic($channelName)
{
    return $this->topicPrefix.'channel/'.rawurlencode($channelName);
}
```

## Cookie 域名与错误提示

由于 Mercure hub 常常运行在与应用不同的子域名下（例如 `mercure.example.com` 对 `app.example.com`），当共享 Cookie 域名解析失败时，驱动会抛出信息明确的异常：

```php theme={null}
throw new BroadcastException(sprintf(
    'Mercure error: %s. Adjust the Mercure "public_url" configuration value so the hub [%s] shares a registrable domain with the application host [%s].',
    rtrim($e->getMessage(), '.'), $this->hub->getPublicUrl(), $request->getHost()
), 0, $e);
```

如果你使用了 `__Secure-` 或 `__Host-` 前缀的 Cookie 名称，驱动还会在启动时校验 `public_url` 必须为 HTTPS：

```php theme={null}
if (str_starts_with($hub->getCookieName(), '__') &&
    parse_url($hub->getPublicUrl(), PHP_URL_SCHEME) === 'http') {
    throw new InvalidArgumentException(/* ... */);
}
```

## 在 Reverb、Pusher/Ably 与 Mercure 之间取舍

| 驱动            | 传输方式       | 基础设施                   | 端到端加密                   |
| ------------- | ---------- | ---------------------- | ----------------------- |
| Reverb        | WebSocket  | 需自建服务器                 | 无                       |
| Pusher / Ably | WebSocket  | SaaS                   | 无                       |
| Mercure       | SSE (HTTP) | 自建 hub 或 FrankenPHP 内置 | 有（`private-encrypted-`） |

Mercure 适合那些不需要持久双向连接的场景——例如通知、进度反馈或以服务端向客户端单向推送为主的类聊天用例。如果你已经在使用 FrankenPHP，完全无需额外基础设施即可运行。

## 相关页面

* [广播基础](/zh-CN/broadcasting)
* [Laravel Cloud](/zh-CN/blog/laravel-cloud)（基于 FrankenPHP 的运行环境）


## Related topics

- [广播](/zh-CN/broadcasting.md)
- [进阶主题](/zh-CN/advanced/index.md)
- [限流的自定义](/zh-CN/advanced/rate-limiting.md)
- [开始学习 Laravel 前需要具备的知识](/zh-CN/true-tutorial.md)
- [Laravel Sentinel — 路由保护中间件调查](/zh-CN/blog/sentinel-introduction.md)
