Mercure订阅模型本质是主题匹配而非点对点寻址,客户端需主动订阅唯一topic URI(如/users/123),服务端向该URI发布消息实现单用户推送,依赖JWT鉴权确保订阅权限。

Mercure 的订阅模型本质是「主题匹配」,不是「点对点寻址」
FrankenPHP 内置的 Mercure 协议本身不支持直接向 user_id=123 这样的具体用户 ID 推送——它只认 URL 风格的主题(topic)URI,比如 https://api.example.com/users/123 或 /notifications/user-123。所谓“推给指定一个人”,其实是让那个用户**主动订阅一个只有他该听的 topic**,服务端再往那个 topic 发布消息。
要实现「单用户精准推送」,必须控制前端订阅路径
后端不能靠“找人”来发,而要靠“约定路径”来收。关键在客户端初始化 Mercure 订阅时的 URL 是否具备唯一性:
- ✅ 正确做法:前端用带用户标识的 topic 订阅,例如
new EventSource('/.well-known/mercure?topic=https%3A%2F%2Fapi.example.com%2Fusers%2F' + userId) - ❌ 错误做法:所有人订阅同一个通配路径如
/.well-known/mercure?topic=/notifications/all,然后靠前端 JS 过滤——这等于广播+浪费带宽 - ⚠️ 注意:Mercure Hub 默认不校验 topic 权限,必须配合 JWT 的
subscriber声明或自定义 auth middleware,否则别人伪造 URL 也能收到本该私有的消息
Symfony Notifier 的 MercureChannel 如何做到“按 recipient 推”
它不是绕过 Mercure 模型,而是把「用户 ID → topic 路径」的映射逻辑封装好了。前提是你的 Notification 对象实现了 getRecipientId() 方法:
- Notifier 会调用
$notification->getRecipientId(),比如返回'123' -
MercureChannel默认将其转为https://api.example.com/users/123这类 URI(可自定义格式) - 最终调用 Mercure Hub 的
Publish()是往这个 URI 发,而非泛播 - 所以你看到“推给某人”,其实是
getRecipientId()+ 预设 topic 模板 + 前端对应订阅三者协同的结果
别忽略 JWT 认证环节,否则「指定推送」形同虚设
Mercure Hub 收到订阅请求时,如果没做鉴权,任何客户端都能订阅任意 topic。生产环境必须启用 JWT 验证,并在 token payload 中声明 subscriber 字段,且值需与 topic URI 匹配(或满足前缀规则)。例如:
立即学习“PHP免费学习笔记(深入)”;
{"subscriber": "user-123", "mercure": {"subscribe": ["https://api.example.com/users/123"]}}
否则攻击者只要知道 topic 格式,就能伪造请求监听他人通知——这时候你以为的“指定推送”,实际是公开广播。



















