十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

跨域、SSE与WebSocket:实时通信的CORS配置与排查指南

跨域、SSE与WebSocket:实时通信的CORS配置与排查指南 1. 从“请求被拦截”说起跨域到底拦截了什么前两天调试一个实时数据推送服务浏览器控制台又给我甩了一行红色报错“Access to XMLHttpRequest at http://backend:8000/api/stream from origin http://localhost:8080 has been blocked by CORS policy: No Access-Control-Allow-Origin header is present on the requested resource。”这个场景是不是很眼熟项目里同时用了 SSE 和 WebSocket前端、后端、Nginx 三层都有配置跨域问题在开发环境还好一到打包部署就冒出来而且 SSE 和 WebSocket 的表现还不一样排查起来相当磨人。这篇文章就从这次“请求被拦截”开始把跨域、SSE、WebSocket 三者的关系、原理和配置一次讲清楚。内容主要面向负责前端集成接调用的同学也适合后端开发者理解浏览器实时通信的边界。看完你至少能搞清楚三个问题为什么有的请求被拦了、SSE 和 WebSocket 的跨域处理有什么不同、以及遇到同类报错该怎么定位和修复。浏览器里说的“跨域”核心是同源策略。所谓同源是指协议、域名、端口三者完全一致。只要有一项不同比如前端在http://localhost:8080后端在http://backend:8000两边就不是同源。同源策略是浏览器为了安全搞出来的一套规则目的是防止恶意网站通过脚本去读取你在其他网站上的数据。注意这里的关键词是“读取”浏览器并不限制请求本身发出它限制的是页面里的 JavaScript 能不能拿到响应结果。我见过不少刚入行的朋友以为“请求被拦截”是请求根本没发出去。其实不是。很多时候你打开 Network 面板请求已经发出去了服务端甚至已经正常返回了 200但浏览器一看响应头里没有许可跨域的标记就直接把响应数据“扣押”下来不交给页面里的代码。这就好比你托人给外地的朋友寄了一封信信顺利送达朋友也写了回信但送信回来的路上被门卫拦住了因为门卫不认识对方。真正被拦的是“回信”不是“去信”。同源策略下不是所有请求都会被预检。浏览器把请求分为简单请求和复杂请求。简单请求主要满足两个条件请求方法是 GET、HEAD、POST 中的一种且 Content-Type 只能是application/x-www-form-urlencoded、multipart/form-data、text/plain中的一个。这种请求会直接发出浏览器在拿到响应后做校验。如果请求方法不是这三种或者 Content-Type 是application/json又或者带了自定义请求头浏览器就会先发一个 OPTIONS 预检请求问问服务器“我接下来要这么跨域发请求你允许吗”如果服务器没有正确响应 OPTIONS控制台就会报 CORS 错误。1.1 同源策略不是不讲道理要解决跨域本质上是让后端在 HTTP 响应里带上 CORS 相关的头告诉浏览器“这个来源是被允许的”。最基础的响应头是Access-Control-Allow-Origin它表示允许哪些源访问。如果后端返回的是*那就表示任何源都能读如果返回的是具体的http://localhost:8080那就只允许这个源。看起来很简单但实际配置经常踩坑。我遇到过最典型的就是“cors跨域配置错误”后端在设置Access-Control-Allow-Origin时用了*同时又勾选了携带 Cookiecredentials。浏览器规定只要请求里带了凭证Access-Control-Allow-Origin就不能是*必须是明确的源。这样一来你明明配了 CORS浏览器照样拦截而且报错信息还特别具有迷惑性经常是“The value of the Access-Control-Allow-Origin header in the response must not be the wildcard * when the requests credential mode is include”。除了这个还有一个容易忽略的点是预检请求的匹配。当浏览器发起 OPTIONS 预检时后端不仅要返回Access-Control-Allow-Origin还要返回Access-Control-Allow-Methods和Access-Control-Allow-Headers这两个值必须覆盖前端实际要用的方法和请求头。我见过有的项目后端只配置了GET, POST结果前端发起一个DELETE请求预检直接失败控制台一直在报 CORS后端日志却干干净净半天排查不出来。所以排查跨域问题时先别急着改业务代码打开 OPTIONS 预检请求把返回的响应头和浏览器期望的值逐一比对往往很快能定位。1.2 CORS 配置背后的 HTTP 语义CORS 说白了就是一组 HTTP 响应头的约定。服务端需要在响应里明确告诉浏览器允许哪个来源、允许哪些方法、允许哪些请求头、是否允许携带凭证。浏览器拿到这些信息再做判断满足就放行不满足就拦截。很多人把这组头当成“配置项”背下来却不知道它们各自的语义出问题时就只能瞎试。拿最常见的Access-Control-Allow-Origin来说它的取值可以是具体的源也可以是通配符*。但在实际项目中只要涉及登录态、Cookie 或者 authorization 头就不能用*。因为允许跨域携带凭证时浏览器要求服务端必须明确知道是哪个源否则 Cookie 会被恶意网站“借用”。Access-Control-Allow-Methods和Access-Control-Allow-Headers也很关键。预检请求本质上是浏览器替前端问一句话“我这次跨域请求能不能用这个方法能不能带这些头”服务端的回应必须精确匹配。比如前端请求头里带了X-CSRFToken但服务端只允许content-type预检照样失败。正确做法是把实际需要的头都列进去并且保证新增业务请求时CORS 配置也会同步更新。2. SSE 与 WebSocket实时通信的两种姿势SSE 全称 Server-Sent Events翻译过来是服务器发送事件。它是一种基于 HTTP 的服务器推送技术客户端用EventSource接口发起一个长连接服务器可以持续不断地往这个连接里写数据。和 WebSocket 相比SSE 最大的特点是单向只能服务器往客户端推客户端想发消息得另开一个普通的 HTTP 请求。SSE 的请求本质上还是一个 HTTP GET 请求只不过响应类型是text/event-stream。既然是 HTTP 请求它就天然受同源策略约束跨域时同样需要 CORS。很多前端同学用EventSource时报 CORS 错第一反应是去改后端 WebSocket 的跨域配置这是不对的。SSE 不走 WebSocket 的握手协议它需要的是普通的 CORS 响应头。WebSocket 和 SSE 最大的不同是它提供全双工通信客户端和服务端可以互相实时发消息。WebSocket 的建立过程要先通过 HTTP 发起一个 Upgrade 请求把协议从 HTTP 切换到 WebSocket。这个“握手”请求和普通 HTTP 请求不太一样跨域规则也不一样。很多开发者会把 WebSocket 和 CORS 混在一起其实不对。WebSocket 的握手并不是由 CORS 机制控制的而是由服务端主动校验请求头里的Origin字段来实现。浏览器在发起 WebSocket 握手时会自动带上发起页面的来源也就是Origin头。服务端看到这个头之后可以决定接受或拒绝连接。如果你在服务端没有做任何 Origin 校验大多数实现是默认接受的所以跨域 WebSocket 也能连得上但如果你写了校验逻辑而白名单里没配前端域名握手就会返回 403浏览器控制台就会显示“Error during WebSocket handshake”之类的信息。因此解决 WebSocket 跨域问题重点不是配 CORS而是检查服务端对 Origin 的处理。2.1 SSE 推送单向但轻量EventSource 的跨域坑除了基础配置SSE 跨域还涉及凭证问题。如果 SSE 接口需要携带登录 Cookie前端要这样写const source new EventSource(/events, { withCredentials: true });一旦设置了withCredentials后端响应里的Access-Control-Allow-Origin就不能再用*必须写成具体的源地址否则浏览器照样拦截。这一点和普通 AJAX 请求的规则是一样的。SSE 还有一个和普通接口不太一样的点它的连接是持续性的。普通接口请求完就断SSE 一旦建立就会一直保持直到服务端关闭或网络异常。因此它对于网关、代理层的超时配置特别敏感。很多团队在生产环境用 Nginx 做反向代理默认proxy_read_timeout是 60 秒如果服务端 60 秒内没有推送任何数据连接就会被 Nginx 切断。这时前端EventSource会触发onerror并自动尝试重连但重连一次断一次用户体验就很差。我在实际项目里验证过SSE 更适合做“低频但持续”的推送比如通知数量变化、任务状态更新、大屏数据刷新。这类场景数据量不大但对实时性有一定要求SSE 的轻量优势非常明显。它不需要额外维护一套独立的协议层只要后端能按一定频率输出文本流就行。2.2 WebSocket双向通信的王者但握手也会被拦WebSocket 的优势在于双向和高效。它一旦握手成功客户端和服务端之间就是一条持久连接可以互相发送文本帧、二进制帧没有普通 HTTP 请求那种“一应一答”的限制。在线聊天、协作文档、实时游戏这类场景WebSocket 是绕不开的选择。但 WebSocket 的跨域处理经常让人困惑。有些项目在 Nginx 层加了 CORS 响应头以为 WebSocket 就能跨域了结果发现握手一样失败。实际上 WebSocket 握手不读 CORS 头它读的是Origin头。浏览器在发起握手请求时会把来源网站写进Origin服务端如果实现了 Origin 白名单校验就会在握手响应里返回 403如果没实现通常就默认接受。我见过不少团队在服务端完全没做 Origin 校验导致线上 WebSocket 可以被任意网站页面发起连接。虽然 WebSocket 协议本身有掩码机制但服务端如果不做鉴权恶意页面一样可以往你的服务发消息。正确做法是在握手阶段校验Origin并对连接做 token 或会话鉴权不能把安全全部寄托在浏览器同源策略上因为 WebSocket 根本不归同源策略管。2.3 选型建议什么时候用 SSE什么时候用 WebSocket我经常被问到实时推送到底用 SSE 还是 WebSocket我的建议是先看需求再看成本。如果你的场景主要是服务端主动向客户端推送数据比如工单提醒、通知中心、大屏数据刷新SSE 就够了它的协议简单基于 HTTP天然支持自动重连和断点续传通过Last-Event-ID后端实现也不复杂。但如果你的场景需要双向交互比如在线聊天、协同编辑、实时游戏那就必须用 WebSocket。下面这张表可以帮助你快速决策对比维度SSEWebSocket通信方向单向服务器推送到客户端双向客户端和服务端互推底层协议HTTPHTTP Upgrade 到 WebSocket 协议跨域处理走标准 CORS需要配置Access-Control-Allow-Origin服务端校验Origin头不是 CORS自动重连原生支持自动重连需要自己实现重连逻辑传输内容服务端文本数据支持多行事件文本和二进制都可以协议复杂度低简单 HTTP高有帧、掩码、控制帧等概念适用场景通知、行情、日志流等单向推送聊天、协作、游戏等双向交互选型也不能只看能力还要看团队的技术栈。如果后端是 Java 或 Node.jsWebSocket 的生态和示例都很成熟如果后端是 DjangoSSE 和 WebSocket 也都有对应的方案但 WebSocket 需要额外部署通道层比如 Django Channels 和 Redis复杂度会高不少。轻量级的推送需求真没必要上 WebSocket。3. 实战解决一次“请求被拦截”的完整排查过程说一下我遇到的那次真实情况。项目是 Vue 3 Vite 前端后端是 Django中间用 Nginx 做反向代理。前端部署在https://app.example.com后端接口走/apiSSE 走/eventsWebSocket 走/ws。本来开发环境是通过 Vite 代理访问后端一切正常但换成打包部署之后各种跨域问题就冒出来了。打开页面后控制台出现了三类报错。第一类是普通接口的 CORS 报错网络请求能发出但被浏览器拦截了。第二类是 SSE 的连接报错打开 Network 面板能看到/events的请求一直处于 pending 状态过一会儿就失败控制台提示“EventSources response has a MIME type (text/html) that is not text/event-stream.”。第三类是 WebSocket 的握手失败报“WebSocket connection to wss://app.example.com/ws/chat/ failed: Error during WebSocket handshake: Unexpected response code: 403”。这三种报错集中在一起排查的第一反应可能是“后端跨域没配好”但实际上背后的原因各不相同。正确做法是分开看一个请求一个请求地追。普通接口看 CORS 响应头SSE 看 Content-Type 和代理超时WebSocket 看握手状态码和 Origin 校验三者并不同源。3.1 现场还原报错信息与现象我总结了一个三步定位法现在也一直这么用。第一步打开浏览器开发者工具的 Network 面板把请求类型筛选成 Fetch/XHR 和 WS逐个检查哪些请求被阻塞。普通接口和 SSE 都是 Fetch/XHR 类别WebSocket 是 WS 类别。注意 SSE 的请求虽然挂着text/event-stream但它的网络类型还是 Fetch/XHR别漏了。第二步点开每个失败的请求查看请求头里的Origin和响应头里的Access-Control-Allow-Origin。如果请求头里有 Origin而响应头里没有这行说明后端没有正确配置 CORS。如果响应头有但值与页面源不匹配说明白名单配置有问题。第三步针对 WebSocket 请求看握手返回的状态码。403 通常是服务端 Origin 校验失败404 可能是路径不对或代理没转发到后端101 才是升级成功。如果一直停留在 101 之前的阶段多半是代理层没有把 Upgrade 头传过去。三步走完就能把问题拆成两块CORS 相关的去查后端配置WebSocket 握手失败的去查服务端 Origin 校验和代理转发规则。这样不会眉毛胡子一把抓。3.2 后端 CORS 配置的正确姿势Django / SpringBoot 示例先说 Django。社区最常用的方案是django-cors-headers安装之后在settings.py里加两处配置# settings.py INSTALLED_APPS [ # ... corsheaders, ] MIDDLEWARE [ # 注意建议放在最前面尤其是放在 CommonMiddleware 之前 corsheaders.middleware.CorsMiddleware, # ... ] CORS_ALLOWED_ORIGINS [ https://app.example.com, http://localhost:8080, ] CORS_ALLOW_CREDENTIALS True CORS_ALLOW_METHODS [GET, POST, OPTIONS, DELETE] CORS_ALLOW_HEADERS [authorization, content-type, x-csrftoken]这里有个细节CorsMiddleware的位置一定要放在能够影响所有响应的地方最好放在CommonMiddleware和WhiteNoiseMiddleware前面否则部分响应可能来不及加响应头。我踩过一次坑把中间件放在最后静态文件和某些异常响应照样没有 CORS 头。如果后端是 SpringBoot可以用 WebMvcConfigurer 配置全局跨域Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOrigins(https://app.example.com, http://localhost:8080) .allowedMethods(GET, POST, OPTIONS, DELETE) .allowedHeaders(*) .allowCredentials(true); } }注意.allowedOrigins()里不能用*因为后面开启了allowCredentials(true)。如果你只是临时调试可以在本地用CrossOrigin注解但生产环境还是建议统一用全局配置方便后续维护。3.3 前端代理也是一种解法Vue devServer 配置开发环境最省事的跨域方案就是前端代理。Vite 的server.proxy配置比 webpack-dev-server 简洁很多// vite.config.js export default { server: { proxy: { /api: { target: http://backend:8000, changeOrigin: true, }, /events: { target: http://backend:8000, changeOrigin: true, }, /ws: { target: ws://backend:8000, ws: true, changeOrigin: true, }, }, }, };这里有个关键点WebSocket 的代理必须设置ws: true否则浏览器发起的 WebSocket 握手不会被转发到后端Vite 只会把它当成普通 HTTP 请求处理。对比之下SSE 其实只是一个普通 HTTP GET所以/events的代理和/api一样就行。但要注意前端代理只解决开发环境。打包部署后前端静态资源由 Nginx 托管浏览器访问的是 Nginx 的域名这时就需要在 Nginx 里配置反向代理把/api、/events、/ws都转到后端的真实地址。很多“vuedjango打包部署后无法跨域”的问题其实就是生产环境 Nginx 没有配代理前端代码里写的是后端内网地址或者代理路径不对导致请求打到了 404。简单说跨域问题不是只在后端加几个头就能一劳永逸代理层必须同步跟上。4. SSE 和 WebSocket 的实战配置与踩坑SSE 看着简单实际配置起来有不少隐藏坑。第一个坑是响应头必须设置正确。服务端返回的Content-Type一定要是text/event-stream否则浏览器会报“EventSources response has a MIME type...”。除此之外还要设置Cache-Control: no-cache避免中间代理或浏览器缓存事件流导致数据不实时。如果前面还有 Nginx建议再加X-Accel-Buffering: no关闭代理层的缓冲。Django 里返回 SSE 流的一种常见写法是使用 StreamingHttpResponsefrom django.http import StreamingHttpResponse import time def event_stream(request): def generate(): while True: yield fdata: {time.time()}\n\n time.sleep(1) response StreamingHttpResponse(generate(), content_typetext/event-stream) response[Cache-Control] no-cache response[X-Accel-Buffering] no return response这里的输出格式要严格按照 SSE 规范data:开头两个换行符结束一条消息。如果你要发多段内容可以用多个data:行以\n\n收尾。规范只是看起来简单但实际生成时少写一个换行客户端就一直没有消息这种问题很难排查。4.1 SSE 配置细节Content-Type、缓存、重连与超时SSE 的第二个坑是超时。SSE 连接本质上是长连接中间任何一层空闲超时都会把它断开。我遇到过一个非常经典的报错stream disconnected before completion: idle timeout waiting for sse。第一次看到这个报错完全摸不着头脑后来查下来发现是阿里云负载均衡默认空闲超时是 4 秒而我们的 SSE 服务在 5 秒内没有推送任何数据网关就把连接断了。解决办法是加心跳服务端每隔 15 秒发一个: keep-alive\n\n注释行告诉代理“连接还活着”同时把代理层的proxy_read_timeout调大比如60s。心跳代码可以这样加import time def generate(): while True: yield : keep-alive\n\n # 注释行不能携带业务数据 time.sleep(15) # 这里再推送真正的业务事件:开头的数据行是 SSE 协议的注释客户端不会把它当成业务消息但代理和负载均衡看到网络包在流动就不会轻易断开连接。如果你们的服务本来就有定期推送的数据那可以不用额外心跳但为了对抗“空闲超时”最好还是统一加一个。SSE 的前端重连也值得留意。EventSource自带自动重连功能连接断开后会默认等待几秒再重试。但如果你在开发环境通过 Vite 代理调试断开重连会比较频繁控制台会刷出一堆报错。这时别慌确认后端日志里 SSE 是否一直在正常 generate 数据即可。如果后端日志有输出说明问题出在浏览器到代理这一段优先查代理超时和缓存配置。4.2 WebSocket 配置细节Origin 校验、心跳、子协议WebSocket 的配置比 SSE 多一层握手环节服务端要在握手时决定是否接受这个连接。很多框架默认接受任意来源的 WebSocket 连接这在生产环境是很危险的因为任何网站都能往你的服务发消息甚至利用已有登录态做数据窃取。正确做法是校验握手请求里的Origin头。以 SpringBoot 整合 WebSocket 为例假设你已经实现了TextWebSocketHandler接下来要加一个握手拦截器public class OriginHandshakeInterceptor implements HandshakeInterceptor { private final ListString allowedOrigins List.of(https://app.example.com); Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, MapString, Object attributes) { String origin request.getHeaders().getOrigin(); return origin ! null allowedOrigins.contains(origin); } }然后在注册 WebSocket handler 时把拦截器挂上Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new ChatHandler(), /ws/chat) .addInterceptors(new OriginHandshakeInterceptor()) .setAllowedOrigins(https://app.example.com); } }注意setAllowedOrigins也不能设为*如果开启了凭证模式同样有安全限制。除了 OriginWebSocket 还有一个容易被忽略的“子协议”subprotocol概念。客户端可以在握手时通过Sec-WebSocket-Protocol头声明要使用的子协议服务端在同一个响应头里返回它支持的那个就可以实现类似“协议版本协商”的效果。举个例子const ws new WebSocket(wss://app.example.com/ws/chat, chat-protocol);服务端如果认识chat-protocol就在响应头里带上Sec-WebSocket-Protocol: chat-protocol。如果服务端返回的子协议和客户端不一致浏览器会主动断开连接。有些团队在接入 SpringBoot 的ServerEndpoint时会忽略这个头导致 WebSocket 连接握手总会失败控制台报错却很隐晦需要抓包才能看到握手头的问题。WebSocket 连接建立后还要注意心跳和超时。大部分服务端框架会在一定时间内没有消息就关闭连接比如 Spring 的WebSocketHandler默认会有会话超时设置。前端和后端可以约定一种心跳消息比如每 30 秒发一个ping服务端回一个pong这样既能保活也能快速探测死连接。前端可以通过定时器发送心跳收到onclose后再做重连避免连接悄悄断掉后客户端还蒙在鼓里。4.3 跨域与安全不要只会开 * 通配最后我想单独强调一下安全。SSE 和 WebSocket 都是长连接一旦出现跨域配置漏洞受影响的不只是某个接口而是整个实时通道。如果你在 CORS 配置里图省事直接写Access-Control-Allow-Origin: *同时又不需要携带凭证普通接口还好但 SSE 如果允许任意源连接恶意网站就可以在用户已登录的情况下读取用户所有实时推送的数据。WebSocket 更严重因为它能双向通信恶意来源甚至能伪造消息写入服务端。所以我在生产环境的基本准则只有三条第一CORS 白名单必须是明确的域名不要用*第二WebSocket 握手必须校验 Origin而且白名单要单独维护不能和 CORS 混在一起第三涉及敏感操作的实时消息协议层最好再加一层鉴权比如 WebSocket 连接后用 token 做身份确认而不是只依赖握手时的 Cookie。5. 常见问题与排查技巧实录把我在实际项目中遇到过的、网上高频出现的跨域、SSE、WebSocket 问题整理成一张表遇到类似情况可以直接对着查现象根本原因解决方案控制台报 CORS且响应头没有Access-Control-Allow-Origin后端未配置 CORS 或配置未生效在网关/后端统一加 CORS 响应头请求头有 Origin响应头有Access-Control-Allow-Origin: *但携带了 Cookie凭证模式下不允许通配符改为具体来源域名设置Access-Control-Allow-Credentials: true预检请求 OPTIONS 返回 404后端路由没有处理 OPTIONS 请求对预检请求放行返回 200SSE 响应类型是text/html后端没有返回text/event-stream设置 Content-Type 为text/event-stream、Cache-Control: no-cacheSSE 连接被代理断开报 idle timeout代理层空闲超时过短加心跳注释行调大代理超时关闭缓冲WebSocket 握手返回 403服务端 Origin 校验失败或代理拒绝检查白名单确认 Host、Origin、Upgrade 头都正确WebSocket 握手返回 404路径没配对或反向代理未转发 WebSocket检查代理配置确保ws/wss是开启状态WebSocket 连接成功后很快断开心跳机制缺失或超时配置太短实现心跳发送 ping/pong 帧Vue Django 打包部署后接口无法访问生产环境代理缺失或路径错误在 Nginx 中配置反向代理前端请求统一走同源路径5.1 问题速查表排查跨域和实时连接问题光看浏览器控制台不够我常用的工具有这几个。第一个是 curl。很多人喜欢用 curl 先测试 SSE 接口是否正常。比如在终端里执行curl -N -H Accept: text/event-stream http://backend:8000/events如果终端里能看到持续输出的data:行说明服务端 SSE 是通的问题可能出在代理层或前端 EventSource 配置上。如果连 curl 都没输出那就得先看后端代码和日志别去折腾前端。第二个是 Postman。新版 Postman 已经支持 WebSocket 请求可以在请求头里自定义 Origin用来模拟浏览器环境下的跨域握手。我在排查“WebSocket 连接 403”时就是先用 Postman 分别带和不带 Origin 头测试确认服务端是否对 Origin 做了校验很快定位到问题。第三个是浏览器开发者工具的 Network 面板。对于 SSE可以直接看响应头和响应体里的内容确认服务器持续发送的数据格式是否正确对于 WebSocket切到 WS 标签页能看到每一帧的 payload 和 type方便确认 ping/pong 是否正常。如果发现很久没有帧数据大概率是心跳没实现或被代理吞了。5.2 排查工具与调试技巧这些心得不是文档里能看到的纯属踩过坑之后总结出来的经验。第一跨域问题不要在开发环境“硬扛”。开发时可以用代理但不能因此忽略生产环境的跨域场景。比较稳的做法是让开发环境的代理路径和生产 Nginx 保持一致前后端联调时就能提前暴露路径问题。第二报错文字不要只看第一眼。stream disconnected before completion: idle timeout waiting for sse和blocked by CORS policy这两类报错看似差别很大但都可能导致 SSE 连接失败。排查时要把网络链路拆开看先确认服务端有没有输出再逐层检查代理、负载均衡、Nginx 超时配置不要一上来就改代码。第三CORS 配置变更后浏览器缓存可能让你产生“没生效”的错觉。有些接口被浏览器缓存了修改后端 CORS 后旧响应头还在缓存里要强制刷新或清缓存再验证。我遇到过几次改完配置还是报错折腾半天发现浏览器缓存的问题浪费了不少时间。第四如果项目用的是 Djangodjango-cors-headers的中间件顺序非常重要。网上一堆教程让你直接放最前面但最前面不一定适配你项目里其他中间件。建议用测试请求验证所有路径的响应都带有 CORS 头尤其要检查 404、500 这类非正常响应否则错误响应被浏览器拦下来前端拿到的还是“请求被拦截”。5.3 我的几条独家心得这次排错之后我把项目的跨域方案统一整理成了一页配置开发环境走前端代理生产环境走 Nginx 反向代理后端明确白名单来源SSE 加上心跳和X-Accel-BufferingWebSocket 校验 Origin 和子协议。后来团队新同事再看这套配置就不会手足无措了。希望这些踩坑记录能帮你少走几步弯路下回再看到blocked by CORS policy或403 during WebSocket handshake能一眼知道该往哪查。
返回列表