FrankenPHP的Mercure JWT必须手动签发:PHP需用HS256算法、与Caddyfile逐字节一致的密钥,调用openssl_sign()或firebase/php-jwt生成含mercure.subscribe/publish绝对URI数组的token,不可依赖环境变量自动生成。

FrankenPHP 的 Mercure JWT 必须手动签发,不能靠环境变量自动生成
FrankenPHP 不会自动为你生成 subscriber_jwt 或 publisher_jwt,所有 JWT 都得由 PHP 代码显式构造并签名。Caddyfile 里只负责校验,不参与生成。常见错误是以为设了 MERCURE_PUBLISHER_JWT_KEY 环境变量,PHP 就能“自动拿到 token”——实际完全不会。
你必须在 PHP 中用 openssl_sign() 或 firebase/php-jwt 手动编码 payload、签名、base64url 编码。密钥字符串要和 Caddyfile 中 publisher_jwt / subscriber_jwt 指令的密钥**逐字节一致**(包括换行符、空格、是否 base64 编码)。
- 推荐先用
base64_decode()处理密钥再传给openssl_sign(),避免因编码差异导致验证失败 - 签名算法强制为
HS256,FrankenPHP Mercure 模块不支持RS256或ES256 - payload 中
mercure.subscribe和mercure.publish字段必须是字符串数组,且每个元素是绝对 URI(如"https://example.com/notifications"),不能是相对路径或通配符中间嵌套
subscriber_jwt 和 publisher_jwt 密钥不能混用
前端订阅用的 token 必须由 subscriber_jwt 密钥签发,后端发布用的 token 必须由 publisher_jwt 密钥签发。两者可以相同,但语义和用途严格分离——这是 Mercure 协议层的安全设计,不是 FrankenPHP 的限制。
如果你把 publisher_jwt 密钥直接暴露给前端 JS 去签发订阅 token,就等于开放了任意主题的推送权限,极易被滥用。
立即学习“PHP免费学习笔记(深入)”;
- Caddyfile 中
subscriber_jwt配置只用于校验订阅请求,不参与发布流程 -
publisher_jwt配置只用于校验 POST 到/.well-known/mercure的发布请求 - 开发时可用同一密钥快速验证逻辑,但上线前必须拆分:subscriber 密钥可适度放宽(如加 exp),publisher 密钥必须严格管控、仅限服务端访问
JWT payload 字段名和结构必须精确匹配
FrankenPHP Mercure 模块只解析 payload 中的 mercure.subscribe 和 mercure.publish 字段,其他字段(如 exp、nbf、iss)它完全忽略。但你仍应设置 exp,否则 token 永不过期,存在长期泄露风险。
字段值不是自由字符串,而是严格校验的字符串数组:
- ✅ 正确:
"mercure": {"subscribe": ["https://example.com/users/123"]} - ❌ 错误:
"mercure": {"subscribe": "https://example.com/users/123"}(不是数组) - ❌ 错误:
"mercure": {"subscribe": ["/users/123"]}(非绝对 URI) - ❌ 错误:
"mercure": {"subscribe": ["https://*.example.com/*"]}(通配符不能出现在域名部分)
主题 URI 必须字节级一致:前端订阅 https://example.com/notify,后端发布时也必须用完全相同的字符串,大小写、末尾斜杠、协议都算在内。
测试 JWT 是否生效最直接的方法是 curl 订阅
不要等前端连上再排查,用 curl 直接模拟 EventSource 请求,看返回状态和响应头:
curl -v -H "Authorization: Bearer <your_token>" \ "http://localhost:8080/.well-known/mercure?topic=https://example.com/notify"
关键观察点:
- 返回
200 OK且响应头含Content-Type: text/event-stream→ token 校验通过,订阅建立 - 返回
401 Unauthorized→ JWT 签名失败或密钥不匹配 - 返回
403 Forbidden→ token 有效但mercure.subscribe不包含该 topic - 返回
404 Not Found→ Caddyfile 没启用mercure指令块,或路径配置错误
这个步骤比改前端代码快得多,且能排除浏览器 EventSource 缓存、CORS、HTTPS 降级等干扰因素。



















