Swoole 的底层特性完成 WebSocket 的握手控制、连接鉴权
在 think-swoole 架构中,底层的 HTTP 协议升级(即握手过程)已经被框架封装,开发者通常不需要(也不建议)去重写底层的 onHandshake 回调,而是通过框架提供的**事件系统(Event)**来拦截和处理业务。 [1, 2]
一、 核心:握手阶段的安全鉴权与 UID 绑定 [1]
在标准的实时应用(如聊天室、通知系统)中,我们通常需要限制未登录用户连接,并将用户的 UID 与 Swoole 的客户端连接 ID (fd) 进行映射。 [1, 2, 3]
1. 前端携带 Token 发起握手 [1]
前端在发起 ws:// 握手请求时,无法像标准 HTTP 那样自定义 Headers(浏览器原生 WebSocket API 的限制)。标准的做法是**将 Token 作为 URL 参数或子协议(Subprotocol)**传给后端: [1, 2, 3, 4]
javascript
// 做法:通过 URL 查询参数将 token 发送给 TP6
const token = "your_jwt_token_here";
const ws = new WebSocket(`ws://127.0.0.1:9501?token=${token}`);
请谨慎使用此类代码。
2. 后端在 Connect 事件中做登录拦截 [1]
当 Swoole 收到客户端的握手请求时,框架会解析请求并触发 swoole.websocket.Connect 事件。此时底层连接虽然已经建立,但你可以在这里直接把非法连接直接断开。 [1, 2, 3, 4]
修改或创建你的监听器 app\listener\WsConnect.php: [1]
php
namespace app\listener;
use think\Container;
use think\swoole\Websocket;
class WsConnect
{
public function handle($event)
{
// 1. $event 实际上就是底层的 Swoole\Http\Request 请求对象
// 我们可以直接获取前端握手时带过来的 GET 参数
$token = $event->get['token'] ?? '';
$fd = $event->fd; // 当前客户端的唯一 fd
// 2. 执行你的 Token 鉴权逻辑(示例)
$uid = $this->checkTokenAndGetUid($token);
if (!$uid) {
// 鉴权失败,直接拒绝!
echo "客户端 {$fd} 鉴权失败,强制断开。\n";
// 获取 Swoole Server 实例并强制关闭该连接
$server = app('swoole.server');
$server->close($fd);
return false;
}
// 3. 鉴权成功,将 UID 与当前 fd 进行关系绑定
// 推荐做法:存入 Redis 缓存(哈希表)中
$redis = app('redis');
$redis->hSet('ws_user_fd', (string)$uid, $fd);
$redis->hSet('ws_fd_user', (string)$fd, $uid);
echo "用户 UID:{$uid} (fd:{$fd}) 握手鉴权成功并上线。\n";
}
private function checkTokenAndGetUid($token)
{
// 模拟解密 Token 并返回用户 id
if ($token === 'your_jwt_token_here') {
return 10086;
}
return null;
}
}
请谨慎使用此类代码。
二、 进阶:如何主动向指定用户发送消息?
由于用户刷新网页时 fd 会发生改变,你在业务层绝不能直接记录固定的 fd。当需要给某个用户发消息时,应当通过 UID 反查 fd。 [1]
示例:在某个普通的 Controller(如订单支付成功后)给用户推送消息: [1]
php
namespace app\controller;
use think\swoole\Websocket;
class Order
{
public function paySuccess(Websocket $ws)
{
$targetUid = 10086; // 目标接收用户
// 从 Redis 中反查该用户的 fd
$redis = app('redis');
$fd = $redis->hGet('ws_user_fd', (string)$targetUid);
if ($fd && app('swoole.server')->exist($fd)) {
// 使用 think-swoole 提供的 Websocket 实例直接推送到指定 fd
$ws->to((int)$fd)->emit('notification', [
'type' => 'order',
'message' => '您的订单已支付成功!'
]);
}
}
}
请谨慎使用此类代码。
三、 生产环境避坑指南(Nginx 反向代理配置) [1]
在生产环境中,你的站点通常运行在 80 (HTTP) 或 443 (HTTPS) 端口下。你不可能让前端直接暴露出 Swoole 的内部端口(如 9501)。此时,必须配置 Nginx 协助完成握手代理。 [1, 2]
如果前端连接的是 wss://://yourdomain.com,Nginx 的反向代理配置应如下: [1]
nginx
location /ws {
proxy_pass http://127.0.0.1:9501; # 指向你 TP6 Swoole 监听的端口
proxy_http_version 1.1; # 核心:必须使用 HTTP/1.1 才能升级协议
# 核心:透传 Upgrade 和 Connection 头给 Swoole 完成握手
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
# 透传客户端真实 IP
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# 关键防断线配置:避免 Nginx 默认 60 秒没有数据往来就强制关闭连接
proxy_read_timeout 600s;
proxy_send_timeout 600s;
}
请谨慎使用此类代码。
目前你的连接能顺利建立了吗?