publisher_jwt 必须在 Caddyfile 中显式配置,否则所有 POST 到 /.well-known/mercure 的请求均返回 403;其值为纯字符串密钥(≥32 字节),需与 PHP 签名密钥逐字节一致,且 JWT payload 必含 mercure.publish 字符串数组(绝对 URI 或合法通配符),仅支持 HS256 算法。

publisher_jwt 必须在 Caddyfile 中显式声明
FrankenPHP 内置 Mercure 不会自动启用发布能力,publisher_jwt 是硬性开关——没配它,任何 POST 到 /.well-known/mercure 的请求都会直接 403。它和 subscriber_jwt 是对称配置,但作用完全不同:前者控制“谁能发”,后者控制“谁能收”。
常见错误是只配了 subscriber_jwt 就以为能发消息,或者把两个密钥设成不同值导致签名验证失败。
-
publisher_jwt值必须是纯字符串(如!ChangeThisOnProduction!),不能是 base64 编码后的结果,除非你签名时也用了同样编码方式 - 密钥长度建议 ≥ 32 字节;太短(如
123)会导致 JWT 签名被快速暴力破解 - 开发环境可临时用
anonymous允许无鉴权订阅,但publisher_jwt仍不可省——它不控制订阅,只放行发布请求
JWT payload 必须含 mercure.publish 字段且为字符串数组
客户端或 PHP 代码生成的发布令牌,payload 里必须有 mercure.publish,且它的值只能是字符串数组,每个元素必须是绝对 URI(带 scheme + host + path),例如 "https://api.example.com/notifications" 或通配符结尾的 "https://api.example.com/users/*"。
错写成单个字符串、相对路径、空数组、或包含非法通配符(如 https://api.example.com/*/feed),Mercure 都会静默拒绝,返回 403 并不提示具体原因。
立即学习“PHP免费学习笔记(深入)”;
- 通配符
*只允许出现在路径末尾,不能在域名、端口或中间路径段 - URI 必须字节级一致:前端订阅用
https://example.com/msg,发布时就不能写https://example.com/msg/(末尾斜杠差异也会导致匹配失败) - 不要给前端 JS 或用户可控接口发放含
mercure.publish的 JWT——这等于开放任意主题推送权限
PHP 发布时必须用 HS256 算法且密钥完全一致
FrankenPHP Mercure 仅支持 HS256 签名算法,不接受 RS256 或 ES256。哪怕你的 JWT 库默认用 RS256,也得显式指定 HS256,否则签名永远验不过。
密钥一致性是最高频的失败点:Caddyfile 里的 publisher_jwt 值、PHP 签名时传入的密钥、以及环境变量(如果用了)三者必须逐字节相同——包括开头空格、结尾换行、是否 base64 编码。
- 推荐做法:在 PHP 中用
base64_decode()处理密钥字符串后再传给JWT::encode(),Caddyfile 里也存 base64 编码后的密钥,避免隐式编码差异 - 测试密钥是否匹配最简单的方法:用
openssl dgst -sha256 -hmac "your_key" -binary对同一 payload 做签名,对比输出是否一致 - 别依赖
file_get_contents()读密钥文件——它可能带 BOM 或换行符,要用trim(file_get_contents($path))
发布请求 URL 和 Header 必须匹配 Hub 监听地址
FrankenPHP Mercure 默认监听 http://localhost:8080/.well-known/mercure(Caddy 内部地址),但 PHP 进程发出的 curl 请求能否抵达,取决于运行环境网络拓扑:
- Docker 容器内 PHP:不能用
localhost,要改用http://host.docker.internal:8080(Mac/Windows)或http://172.17.0.1:8080(Linux) - 本地开发(非 Docker):若 Caddyfile 绑定
:443,则请求地址应为https://localhost/.well-known/mercure,且需处理自签名证书(CURLOPT_SSL_VERIFYPEER => false) - Header 中
Authorization: Bearer <token>必须存在,且Content-Type: application/json不可少;漏掉任一,返回 401 或 415
最容易被忽略的是:FrankenPHP 的 Mercure Hub 在未启用时,/.well-known/mercure 路径根本不存在(不是 403,而是 404),所以先确认 Caddyfile 里 mercure { ... } 块已取消注释并正确缩进。



















