Cookie、Session、CORS、WebSocket、SSE 与代理边界
浏览器请求到达应用之前,可能已经经过 CDN、WAF、负载均衡和网关;响应回到浏览器后,还要接受 Cookie、同源策略和 CORS 的约束。WebSocket 与 SSE 又把一次短请求变成长生命周期通道。它们看似是几个分散特性,实质共同回答三个问题:状态归谁所有,哪个边界可以被信任,连接中断后如何恢复。
Cookie 只是客户端携带的凭据容器
Cookie 由用户代理保存,并按 Domain、Path、Secure、SameSite 等规则附加到后续请求。RFC 6265 是现行 RFC 入口;后续 6265bis 尚处于发布流程,工程实现要验证目标浏览器行为,不能把草案细节当成所有客户端已经一致实现。
会话 Cookie 应使用高熵、不可预测的标识,服务端只保存其哈希或映射状态;登录、提权和敏感资料变更后轮换标识,防止 session fixation。HttpOnly 降低脚本读取风险但不阻止浏览器自动携带,不能替代 CSRF 防护;Secure 只允许安全上下文发送;SameSite 限制跨站携带,但 OAuth 跳转、嵌入场景和旧客户端需要单独验证。
Domain 应尽量收窄到 host-only,Path 不是安全隔离边界。删除 Cookie 必须使用与设置时兼容的 Domain 和 Path,并设置过期;只把值改为空可能留下同名不同作用域 Cookie。服务端还应限制 Cookie 总大小与数量,避免 Header 膨胀触发代理拒绝或每次请求浪费带宽。
Session 可以放本机内存、共享存储或自包含令牌。本机内存要求粘性路由,实例故障会丢状态且扩缩容不均;共享存储提供集中撤销但增加一次依赖和热点;自包含令牌减少读取,却使即时撤销、密钥轮换和权限变更传播更复杂。选择依据是状态一致性、撤销时效、故障域与容量,不是“有状态一定落后”。
CORS 是浏览器读取许可,不是服务端授权
同源由 scheme、host、port 共同确定。CORS 让服务器通过响应 Header 告诉浏览器哪些跨源脚本可以读取响应;非浏览器客户端不会被它约束,因此 CORS 不能保护 API。服务端仍要认证、授权、校验 CSRF,并把租户边界落实在业务数据上。Fetch Standard 是浏览器 CORS 行为的规范入口。
携带凭据时不能使用 Access-Control-Allow-Origin: *,必须回显经过白名单精确匹配的 Origin,并返回 Access-Control-Allow-Credentials: true。若响应随 Origin 变化,要设置 Vary: Origin,否则共享缓存可能串用许可结果。白名单不要用脆弱的后缀判断:evil-example.com 不能因为以 example.com 结尾就获准;应解析 Origin 并比较规范化的 scheme、host 和 port。
预检缓存能减少 OPTIONS,但允许方法、Header 或 Origin 策略变化时会有传播延迟。监控要区分预检与实际请求、拒绝原因和 Origin 类别,同时禁止把完整任意 Origin 直接做成高基数指标。
WebSocket 与 SSE 的背压模型不同
WebSocket 在 HTTP 握手后切换为全双工帧协议,RFC 6455 定义文本、二进制、控制帧、分片和关闭握手。它适合客户端与服务端都频繁主动发送的场景,但每条长连接都占用状态、缓冲、心跳和路由资源。服务端必须限制消息大小、分片累计大小、待发送队列和每连接速率;否则慢客户端会让广播消息在内存中无限堆积。
WebSocket 的 ping/pong 用于协议级存活,业务还要定义订阅确认、序号、断线恢复和重复消息。关闭不是直接丢 socket:先发送 close frame,记录 code/reason,停止新消息,给在途消息有限完成时间,再释放底层连接。代理的 idle timeout 必须大于心跳周期,否则健康连接会被周期性切断。
SSE 使用 text/event-stream 在普通 HTTP 响应上持续发送单向事件,HTML Standard 定义 data、event、id、retry 和重连行为。浏览器可在重连时携带 Last-Event-ID,服务端据此从可保留的事件日志恢复。若事件没有稳定 id,重连只能接受丢失或重复;若日志保留时间短于客户端最长离线时间,也必须明确返回“无法续传”并触发全量同步。
代理可能缓冲 SSE,导致服务端已写出但客户端迟迟收不到。响应要禁用不合适的缓冲与压缩策略,定期发送注释心跳,并监控写出到真正刷新的延迟。WebSocket 与 SSE 都需要按连接数、连接年龄、断开原因、发送队列字节、慢消费者和重连率计量,不能只统计握手 101 或初始 200。
examples/backend-development/network-web/state-streaming-proxy/StateStreamingDemo.java 检查带凭据 Cookie、精确 CORS、Vary: Origin 和完整 SSE 事件;ProxyBoundaryDemo.java 证明只有来自受信任代理的转发地址才可覆盖 socket peer:
javac --release 17 --add-modules jdk.httpserver -Xlint:all -Werror examples/backend-development/network-web/state-streaming-proxy/StateStreamingDemo.java examples/backend-development/network-web/state-streaming-proxy/ProxyBoundaryDemo.java
java --add-modules jdk.httpserver -cp examples/backend-development/network-web/state-streaming-proxy StateStreamingDemo
java --add-modules jdk.httpserver -cp examples/backend-development/network-web/state-streaming-proxy ProxyBoundaryDemocookieForwarded=true corsExact=true sseComplete=true varyOrigin=true
untrustedIp=203.0.113.9 trustedIp=198.51.100.7 spoofRejected=true转发 Header 必须形成可审计的信任链
应用直接信任任意 X-Forwarded-For,攻击者就能伪造源 IP;直接信任 X-Forwarded-Proto,就可能绕过 HTTPS 强制、生成错误回调地址或发出缺少 Secure 的 Cookie。入口代理应先删除外部同名字段,再根据实际连接写入规范化元数据;下一跳只信任明确代理网段,并从可信侧向外剥离地址链。
代理还会改变 Host、路径前缀、scheme、客户端证书信息和超时。应用必须知道哪个字段是权威来源。Forwarded Header 参与授权时,要把代理配置、网段变更和入口清洗纳入安全审计;不能把它当作普通业务 Header。
长连接使部署和扩缩容更复杂。实例摘流时先停止接受新连接,通知客户端重连或发送关闭信号,等待有限宽限期,再终止残余连接。若订阅状态只在实例内存,重连到另一实例后必须能从客户端声明或共享状态恢复;依靠粘性会话隐藏状态所有权,故障时仍会暴露。
端到端容量由最慢消费者决定
连接数只是第一维。还要预算每连接堆内状态、内核缓冲、TLS 状态、心跳 QPS、广播扇出、事件日志保留和代理文件描述符。十万连接每条仅排队 64 KiB,就可能产生数 GiB 待发送数据。对慢消费者应采用有界队列,并按业务选择丢弃旧状态、合并更新、断开重同步或反压上游。
安全上,Cookie 和 Authorization 不进入日志;WebSocket 消息与 SSE 数据仍需逐条授权和大小限制;跨域白名单按环境集中管理;代理只接受受信任上游;会话撤销与密钥轮换有明确传播时限。这样一次从浏览器到源站的长链路,才不会在某个“便利 Header”或无界缓冲区上失去控制。
重连协议决定是否丢消息
连接断开只是传输事件,业务恢复需要稳定序号。服务端为每个订阅分配单调游标,客户端确认已应用的最大连续序号;重连时提交订阅条件与游标,服务端从保留日志重放。若游标早于最老保留事件,应明确返回 gap,要求客户端做全量快照后从新基线继续。把“最近一分钟事件”悄悄发给客户端会留下无法察觉的数据空洞。
至少一次重放意味着客户端可能收到重复事件,处理器应按事件 id 去重或让状态更新天然幂等。若消息间存在因果顺序,不能只在多连接间追求更高吞吐而放弃同一 key 的排序。心跳不进入业务序号,避免无意义推进恢复游标。
多级代理中的超时要逐跳对齐
浏览器、CDN、网关和应用各有 idle timeout、最大连接年龄和响应缓冲策略。内层允许 SSE 保持一小时,外层 60 秒无字节断开,客户端就会稳定地每分钟重连。合理配置是应用心跳短于最小 idle timeout,同时给网络抖动留余量;最大连接年龄通过带抖动的主动轮换分散,避免整批连接同一时刻重建。
代理重试长连接握手时也要谨慎。握手请求可能创建订阅或计费状态,重复转发必须有可识别的会话意图;升级成功后的 WebSocket 数据绝不能由普通 HTTP 重试逻辑接管。SSE 初始响应若在 Header 后中断,客户端重连应依靠事件 id,而不是代理盲目拼接两条上游响应。
会话状态的故障演练
应主动验证四个场景:实例被立即终止后,Session 是否仍可读取;共享 Session 存储变慢时,入口是否有 deadline 和隔离;密钥轮换时,新旧令牌能否在重叠窗口正确验证;用户撤销权限后,既有 WebSocket/SSE 是否在规定时间内被重新授权或断开。
鉴权不能只发生在握手。长连接可能存活数小时,期间账号被禁用、租户权限变化或令牌过期。可以在敏感消息上重新授权、周期刷新会话租约,或由权限变更事件主动关闭连接。具体策略取决于撤销时效,但必须有上限和可验证指标。
