通过监听 request 事件来拦截和验证 WebSocket 握手
在 think-swoole 中,如果项目同时开启了 HTTP 服务和 WebSocket 服务,客户端发起 WebSocket 握手时,底层的 Swoole 会优先将其视为一个普通的 HTTP 请求。
如果你直接监听底层的 handshake 事件感到协议计算太繁琐,完全可以通过监听 request 事件来拦截和验证 WebSocket 握手。这种方式更加框架层(Framework-friendly),可以直接利用 ThinkPHP 的 Request 对象,甚至能完美兼容系统的中间件(Middleware)。
以下是具体的实现方案:
🛠️ 核心原理:在 request 事件中识别 Upgrade
当客户端发起 ws:// 连接时,HTTP 请求头中会包含 Upgrade: websocket。我们只需要在 request 事件中拦截这种请求,进行 Token 验证:
验证通过:不作干预,让底层自动放行并升级为 WebSocket。
验证失败:直接通过 HTTP 响应返回错误状态码(如
401),提前熔断。
📝 实现步骤
1. 创建全局或路由中间件(推荐)
在 think-swoole 中,request 事件最终会被路由到 ThinkPHP 的应用层。因此,直接使用 ThinkPHP 的 中间件 是最优雅的拦截方式。
使用命令行生成一个中间件:
bash
php think make:middleware WsAuth
请谨慎使用此类代码。
2. 编写中间件验证逻辑
打开 app\middleware\WsAuth.php,编写如下逻辑:
php
namespace app\middleware;
use Closure;
use think\Request;
use think\Response;
class WsAuth
{
public function handle(Request $request, Closure $next)
{
// 1. 判断当前请求是否是 WebSocket 升级握手请求
$isUpgrade = strtolower($request->header('upgrade')) === 'websocket';
if ($isUpgrade) {
// 2. 获取客户端带来的 Token(支持从 URL、Header 或 Cookie 中获取)
$token = $request->param('token') ?: $request->header('Authorization');
// 3. 执行你的验证逻辑
if (!$this->checkToken($token)) {
// 验证失败:直接返回 401 响应,阻止连接升级
return response('Unauthorized: Handshake Failed', 401);
}
// 【可选】验证成功:可以将解密出的用户 UID 注入到当前的 Request 对象中
// 方便后面的连接建立事件或消息事件读取
$request->macro('user_id', function() {
return 10024; // 假设这是从 token 中解析出的用户ID
});
}
// 验证通过,或者属于正常的 HTTP 网页请求,直接放行
return $next($request);
}
/**
* 验证 Token 合法性
*/
private function checkToken($token): bool
{
// 替换为你的真实鉴权逻辑(如 JWT/Redis 验证)
return $token === 'my_secret_token';
}
}
请谨慎使用此类代码。
3. 注册中间件
为了让中间件生效,你需要将其注册到全局或者特定的 WebSocket 路由中。
方式 A:注册为全局中间件(最简单)
打开 app/middleware.php(如果没有则创建),将中间件加入数组:
php
return [
// ... 其他中间件 ...
\app\middleware\WsAuth::class,
];
请谨慎使用此类代码。
方式 B:通过 config/swoole.php 直接绑定 request 事件(原生 Swoole 风格)
如果你不想过早进入 ThinkPHP 的中间件管线,也可以在 config/swoole.php 中监听底层的 swoole.request(注意需要框架支持或手动在自定义服务中绑定),但通常上面方式 A 的框架级中间件最不易出错且支持标准的响应返回。
💡 如何在后续事件中获取当前连接的用户?
使用 request 事件拦截握手最大的好处是:不需要自己写 Sec-WebSocket-Accept 的加密应答代码,底层会自动处理。
当握手成功后,你可以在 connect 事件(连接成功后触发)中拿到刚刚通过验证的连接。结合 think-swoole的绑定机制,你可以将当前连接的进程号(FD)和用户绑定:
打开 app\listener\WebsocketConnect.php:
php
namespace app\listener;
use think\swoole\Websocket;
use think\facade\Cache;
class WebsocketConnect
{
public function handle(Websocket $websocket)
{
$fd = $websocket->getSender();
// 注意:因为 Swoole 是常驻内存的,你可以在握手成功后
// 将当前 $fd 对应的用户信息记录到 Redis 或系统的缓存中
// 这样在接下来的 WebsocketMessage 事件中,就能根据 $fd 知道是谁在发消息
// 示例:将当前连接和对应的用户 ID 绑定,设置有效期
// Cache::set("ws_fd_{$fd}", $userId, 3600);
}
}
请谨慎使用此类代码。