Laravel Reverb WebSocket 前端收不到事件,认证返回 403?
/broadcasting/auth 返回 403,说明客户端订阅私有频道时未通过授权。订阅没有成功,前端就收不到发往该频道的广播,即使后端已经触发了事件。
从现有代码看,先检查 routes/channels.php 是否注册了 donor.{donorId} 授权规则、文件是否已加载,以及授权时取得的用户是否为目标 donor。仅凭 .env 和 HTTP 路由,还无法确定具体原因。
1. 先补齐私有频道的授权规则
下面的路由接收认证请求,并交给广播组件处理:
Route::post('/broadcasting/auth', function (Request $request) {
return Broadcast::auth($request);
})->middleware(['web', 'auth:web,hospital'])
->name('broadcasting.auth');
谁能订阅 donor.123,通常由 routes/channels.php 中的规则决定,这段路由没有定义该权限。
假设 donor 使用 web guard 登录,频道中的 ID 就是当前用户的认证 ID,可以这样配置:
<?php
use Illuminate\Support\Facades\Broadcast;
Broadcast::channel('donor.{donorId}', function ($user, $donorId) {
return (string) $user->getAuthIdentifier() === (string) $donorId;
}, ['guards' => ['web']]);
检查时确认以下几点:
- 规则名称为
donor.{donorId},与事件中的donor.前缀一致。 - 回调明确返回
true或false。 - 当前用户的 ID 与频道里的 donor ID 属于同一种身份。
如果 donor ID 是另一张表的主键,就需要根据用户与 donor 的关联关系授权,不能直接比较两个 ID。
没有匹配的频道规则、回调返回 false,或所选 guard 无法取得用户,都可能导致 403。
确认 routes/channels.php 已加载
即使文件存在,也要检查其中的规则是否已经注册。广播路由和频道文件的注册位置取决于 Laravel 版本及项目结构,常见位置包括 bootstrap/app.php 和 BroadcastServiceProvider。
可以先运行:
php artisan route:list --path=broadcasting -v
php artisan channel:list
如果当前版本没有 channel:list,直接查看项目中的广播注册代码。
确认 /broadcasting/auth 使用了预期的中间件,且 donor.{donorId} 规则已经加载。还要检查框架和手动代码是否注册了同路径的认证路由。保留一套明确的注册方式,避免请求实际执行的路由与预期不同。
2. 区分路由认证与频道授权
auth:web,hospital
表示请求通过 web 或 hospital 中任一 guard 的身份认证即可。具体能否订阅 donor 频道,仍由频道授权规则决定;授权时使用的身份也需要单独确认。
分别核对:
- donor 登录时使用哪个 guard。
/broadcasting/auth请求中,哪些 guard 能取得用户。- 频道规则是否指定了
guards,最终传入回调的用户是谁。
如果 donor 只通过 web 登录,可以让路由和前面的频道规则都使用 web:
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Broadcast;
use Illuminate\Support\Facades\Route;
Route::post('/broadcasting/auth', function (Request $request) {
return Broadcast::auth($request);
})->middleware(['web', 'auth:web'])
->name('broadcasting.auth');
这段代码适用于采用手动认证路由的项目。如果框架已注册该端点,应调整现有配置,避免重复添加。
如果医院账号也需要订阅 donor 频道,应按业务关系判断权限,例如医院是否有权查看该 donor 的通知。不同身份表中的 ID 可能重复,不能仅凭两个账号的数字 ID 相同就允许订阅。
3. 检查认证请求是否携带登录会话
即使页面显示“已登录”,也要确认 /broadcasting/auth 收到了同一个会话。
在浏览器 Network 面板中打开失败的请求,检查:
- 请求是否发往正确的 Laravel 应用。
- 是否携带登录 Session Cookie。
- 请求体是否包含
socket_id和channel_name。 - 响应正文来自 Laravel,还是代理或其他中间件。
- 是否发生过登录重定向。
对于 donor.123,请求中的频道名通常应为:
private-donor.123
使用 Session 登录时,认证路由应经过 web 中间件,访问域名也应保持一致。例如,登录使用 localhost,认证请求却发往 127.0.0.1,可能导致请求没有携带登录 Cookie。
如果前端与 Laravel 跨域,还要检查 Cookie 属性、请求凭据和 CORS;使用 Sanctum 的项目应按其 SPA 认证方式配置。这些会话和跨域问题无法只靠修改一个 Reverb 参数解决。
不同状态码对应的排查方向也有区别:
| 现象 | 优先检查 |
|---|---|
| 403 | 频道授权失败、无法取得频道授权用户,或其他组件拒绝请求 |
| 401、跳转登录页 | 路由认证未通过 |
| 419 | Session 或 CSRF 校验 |
| 404 | 认证端点路径或路由注册 |
项目的异常处理可能影响具体响应,需要结合响应正文和 Laravel 日志判断。不要通过关闭 CSRF 校验,或把授权回调改成始终返回 true 来绕过问题。
4. 核对频道名、用户 ID 和事件名
题目中的两种频道写法可以对应:
new PrivateChannel('donor.' . $this->donor->id)
window.Echo.private('donor.123');
Echo 会处理私有频道所需的 private- 前缀,routes/channels.php 中仍写:
Broadcast::channel('donor.{donorId}', /* ... */);
不要把这三个位置都改成 private-donor.*。
还要检查浏览器实际执行的代码。下面的 Blade 表达式只有放在经过 Blade 渲染的模板中,才会替换为用户 ID:
window.Echo.private('donor.{{ auth()->user()->id }}');
如果写在普通 .js 文件中,通常无法得到用户 ID。可以先在 Blade 模板中传入数据,再由前端脚本读取:
<script>
window.currentDonorId = @json(auth('web')->id());
</script>
window.Echo.private(`donor.${window.currentDonorId}`)
.listen('.blood-request.created', (data) => {
console.log(data);
});
这个示例同样以 donor 使用 web guard 登录为前提。确认事件构造时传入的 $donor->id 与浏览器订阅的 ID 一致。
事件已经定义:
public function broadcastAs(): string
{
return 'blood-request.created';
}
因此,前端应保留:
.listen('.blood-request.created', ...)
开头的 . 用于监听这个自定义事件名,写法正确。
5. 分开检查 Laravel 认证端点和 Reverb 服务
题目中的两个地址用途不同:
| 地址 | 用途 |
|---|---|
http://localhost:8000/broadcasting/auth | Laravel 验证用户并授权私有频道 |
localhost:8001 | Reverb WebSocket 服务 |
认证请求发往 8000 是合理的,无须因为 WebSocket 使用 8001 就把认证端点也改到 8001。
本机开发时,可以通过启动参数明确指定 Reverb 的监听地址和端口:
php artisan reverb:start --host=127.0.0.1 --port=8001 --debug
使用 reverb:start 启动服务。REVERB_HOST、REVERB_PORT 是 Laravel 向 Reverb 发送广播时使用的连接配置;Reverb 的服务监听地址由 REVERB_SERVER_HOST、REVERB_SERVER_PORT 或启动参数控制,两处需要分别核对。
还要确认:
- Echo 初始化代码实际读取了
VITE_REVERB_*。 - Laravel、Reverb 和前端使用的应用配置相互对应。
- Reverb 的
allowed_origins允许当前页面来源。 - 浏览器能访问配置中的 WebSocket 主机和端口。
如果浏览器运行在另一台设备上,localhost 指向的是那台设备,需要换成浏览器能够访问的服务地址。
修改配置后,可根据实际变更执行:
php artisan config:clear
修改 VITE_* 后,需要重启 Vite 开发服务或重新构建前端资源;修改 Reverb 配置后,需要重启 Reverb 进程。REVERB_APP_SECRET 只应保留在服务端。
排查 /broadcasting/auth 的 403 时,先检查 Laravel 的身份认证和频道授权,再验证这些连接设置。
6. 授权成功后,再检查广播队列
BloodRequestCreated 实现了 ShouldBroadcast。如果使用异步队列连接,触发事件后还需要队列 worker 执行广播任务:
php artisan queue:work
队列连接为 sync 时,无须额外启动 worker。使用异步队列时,还要查看失败任务和 worker 日志,确认广播任务已经成功发送给 Reverb,不能仅凭事件已触发来判断。
修复后,按以下顺序验证:
- WebSocket 连接建立。
/broadcasting/auth返回成功的授权响应。- WebSocket 中出现该频道的
pusher_internal:subscription_succeeded消息。 - 在订阅成功后重新触发事件。
- 检查广播任务执行情况、Reverb 调试输出及浏览器收到的帧。
私有频道订阅成功前发出的广播,通常不会在授权修复后自动补发。每次验证都应等订阅成功后,再触发一个新事件。
备注:内容仅供参考。
