Bottle-WebSocket:如何确保HTTP请求与WebSocket连接同属一个会话?
好问题!其实你当前的代码已经天然保证了WebSocket连接和发起它的HTTP请求属于同一个浏览器会话——不过咱们得先搞清楚背后的原理,再说说怎么确认和加固这个逻辑,毕竟你对HTTP和WebSocket不太熟,我慢慢给你拆解。
首先得明白:WebSocket连接不是凭空建立的,它是基于HTTP协议的「握手升级」来的。
当浏览器要发起WebSocket连接时,会先给服务器发一个特殊的HTTP GET请求(也就是握手请求),这个请求会自动带上当前浏览器会话的所有Cookie、请求头信息——这就是你代码里request.cookies.get('some_key')能拿到数据的原因。
而bottle-websocket插件在处理这个握手请求时,会把HTTP请求的上下文(包括Cookie、请求头、会话标识)直接关联到后续创建的WebSocket连接上。所以你在_serve_websocket里拿到的request对象,就是那个发起握手的HTTP请求的对象,自然属于同一个浏览器会话。
光说原理不够,给你两个实际的验证方法,自己跑一遍就懂了:
方法1:打印日志看关联关系
在你的_serve_websocket方法里加几行日志,把WebSocket连接的标识和Cookie数据打出来:
def _serve_websocket(self, ws): handler = MyHandler() some_data = request.cookies.get('some_key') # 打印WebSocket连接的唯一ID和对应的Cookie值 print(f"WebSocket连接ID: {id(ws)},关联的some_key Cookie: {some_data}") while True: msg = ws.receive() if not msg: # 处理连接断开的情况 break handler.do_sth_on(msg, some_data) ws.send(msg) del(handler)
然后:
- 用同一个浏览器打开你的Web应用,发起WebSocket连接,看控制台日志里的Cookie值是不是和浏览器里的一致;
- 换个浏览器或者打开隐身窗口,再发起连接,你会看到日志里的Cookie值变了——这就证明不同会话的WebSocket连接关联的是不同的HTTP请求上下文。
方法2:用浏览器开发者工具抓包看握手请求
打开浏览器的F12开发者工具,切换到「Network」标签,筛选「WS」类型的请求:
- 点击这个WebSocket请求,看「Headers」里的「Request Headers」,你会看到
Cookie字段里包含你设置的some_key——这就是握手时发送的HTTP请求头; - 再切换到「Frames」标签,后续的WebSocket消息都是基于这个连接的,而这个连接的源头就是刚才带Cookie的HTTP握手请求,所以肯定是同一个会话。
再给你补点基础,帮你彻底搞懂:
- HTTP会话通常靠Cookie(比如会话ID)来标识,浏览器有个规则:同一个域名下的所有请求(包括WebSocket握手的HTTP请求),都会自动带上该域名对应的Cookie;
- WebSocket的握手请求必须满足几个条件:
- 使用
ws://(明文)或wss://(加密)协议; - 请求头里必须包含
Upgrade: websocket和Connection: Upgrade字段,告诉服务器要升级到WebSocket协议; - 还会有
Sec-WebSocket-Key等字段用于协议升级的安全验证;
- 使用
- 服务器收到握手请求后,如果同意升级,就会返回101状态码,之后的通信就切换到WebSocket协议,但这个连接的会话身份已经由握手时的HTTP请求确定了——因为Cookie是浏览器自动带的,同一个浏览器会话(同一个窗口/没清除Cookie的浏览器进程)发起的WebSocket握手,必然带同一个Cookie。
虽然默认逻辑是可靠的,但有些场景需要额外注意:
坑1:跨域场景下Cookie不会自动携带
如果你的WebSocket服务和前端页面不在同一个域名下,浏览器默认不会在握手请求里带Cookie,这时候需要两步配置:
- 前端创建WebSocket时,开启凭证携带:
// 原生JS示例 const ws = new WebSocket('ws://你的websocket域名/ws路径', { withCredentials: true }); - 服务器端配置CORS允许携带凭证,在Bottle里可以加个中间件:
from bottle import response def enable_cors(fn): def _enable_cors(*args, **kwargs): # 替换成你的前端域名,不要用*,否则withCredentials会失效 response.headers['Access-Control-Allow-Origin'] = 'https://your-frontend-domain.com' response.headers['Access-Control-Allow-Credentials'] = 'true' response.headers['Access-Control-Allow-Methods'] = 'GET, POST, OPTIONS' response.headers['Access-Control-Allow-Headers'] = 'Content-Type, Upgrade, Connection, Sec-WebSocket-Key, Sec-WebSocket-Version' # 处理OPTIONS预检请求 if request.method == 'OPTIONS': return response return fn(*args, **kwargs) return _enable_cors # 给你的MyServer应用添加CORS钩子 app = MyServer() app.add_hook('before_request', enable_cors)
坑2:敏感数据不要直接存在Cookie里
如果你的some_key是敏感数据(比如用户ID、权限信息),不要直接存在Cookie里,建议用「会话ID+服务器存储」的方式:
- 浏览器里只存一个随机生成的会话ID(比如
session_id=abc123xyz); - 服务器端用这个会话ID去查询Redis、数据库等存储里的真实用户数据;
- 这样即使Cookie被篡改,没有对应的会话数据,也无法正常处理WebSocket消息,安全性更高。
坑3:WebSocket断开重连的处理
当WebSocket因为网络问题断开重连时,浏览器会重新发起握手请求,这时候依然会自动带上当前会话的Cookie,所以你只需要在重连后的_serve_websocket方法里重新读取Cookie即可,不需要额外的会话绑定逻辑。
内容的提问来源于stack exchange,提问作者DumTux

