前言
在现代 Web 开发中,跨域问题一直是开发者必须面对的挑战。无论是传统的 HTTP 请求还是实时通信的 WebSocket,浏览器的同源策略(Same-Origin Policy)都可能成为功能实现的拦路虎。许多开发者对 HTTP 的跨域解决方案(如 CORS)已经非常熟悉,但面对 WebSocket 的跨域问题时,常常陷入困惑:“WebSocket 不是基于 TCP 的协议吗?为什么也会有跨域限制?”
本文将深入剖析 WebSocket 的跨域机制,并手把手教你如何通过 Spring Boot 实现安全的 WebSocket 跨域连接。
一、WebSocket 跨域问题的本质
1.1 浏览器安全策略的延伸
WebSocket 协议虽然在传输层基于 TCP,但其握手阶段仍通过 HTTP 完成。浏览器在发起 WebSocket 连接时,会携带 Origin
头字段,标识请求来源。服务器若未明确允许该来源,浏览器会拒绝建立连接。
举个例子:
-
前端页面部署在
https://client.com
-
WebSocket 服务端地址为
wss://api.server.com/ws
此时,浏览器会检查api.server.com
是否允许client.com
的跨域请求。
1.2 与 HTTP CORS 的差异
HTTP 的跨域通过预检请求(Preflight)和响应头(如 Access-Control-Allow-Origin
)实现,而 WebSocket 的跨域处理更为简单:
-
握手阶段验证
Origin
头。 -
服务器返回
HTTP 101 Switching Protocols
表示接受跨域。 -
没有预检请求,直接通过响应头控制权限。
1.3 典型的跨域错误场景
若未正确配置,浏览器控制台会抛出错误:
plaintext
WebSocket connection to 'wss://api.server.com/ws' failed: Error during WebSocket handshake: Unexpected response code: 403
二、Spring Boot WebSocket 的跨域解决方案
Spring Boot 提供了两种主流的 WebSocket 实现方案:原生 WebSocket 与 STOMP over WebSocket。以下分别介绍它们的跨域配置方法。
2.1 方案一:原生 WebSocket 跨域配置
2.1.1 实现 HandshakeInterceptor
拦截器
通过拦截握手请求,动态验证 Origin
头:
java
public class CustomHandshakeInterceptor implements HandshakeInterceptor { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) { // 从请求头中获取 Origin String origin = request.getHeaders().getOrigin(); // 定义允许的域名列表(可改为从配置读取) List<String> allowedOrigins = Arrays.asList("https://client.com", "http://localhost:8080"); if (allowedOrigins.contains(origin)) { return true; // 允许握手 } response.setStatusCode(HttpStatus.FORBIDDEN); return false; // 拒绝连接 } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {} }
2.1.2 注册 WebSocket 端点并配置跨域
在配置类中启用 WebSocket 并添加拦截器:
java
@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new MyWebSocketHandler(), "/ws") .addInterceptors(new CustomHandshakeInterceptor()) .setAllowedOrigins("*"); // 谨慎使用通配符 } }
⚠️ 关键注意事项
-
setAllowedOrigins("*")
会允许所有来源,存在安全风险,建议在生产环境中指定具体域名。 -
若同时使用拦截器和
setAllowedOrigins
,拦截器的验证逻辑优先执行。
2.2 方案二:STOMP over WebSocket 跨域配置
对于使用 STOMP 协议的场景(如结合 Spring Messaging),需通过 WebSocketMessageBrokerConfigurer
配置。
2.2.1 基础配置类
java
@Configuration @EnableWebSocketMessageBroker public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer { @Override public void registerStompEndpoints(StompEndpointRegistry registry) { registry.addEndpoint("/stomp/ws") .setAllowedOrigins("https://client.com") .withSockJS(); // 可选,支持 SockJS 回退 } @Override public void configureMessageBroker(MessageBrokerRegistry registry) { registry.enableSimpleBroker("/topic"); registry.setApplicationDestinationPrefixes("/app"); } }
2.2.2 动态 Origin 控制
若需根据请求动态判断,可自定义 DefaultHandshakeHandler
:
java
public class CustomHandshakeHandler extends DefaultHandshakeHandler { @Override protected boolean validateOrigin(HttpServletRequest request) { String origin = request.getHeader("Origin"); // 自定义验证逻辑(例如查询数据库) return isValidOrigin(origin); } } // 在配置类中替换默认 Handler registry.addEndpoint("/stomp/ws") .setHandshakeHandler(new CustomHandshakeHandler());
三、进阶:解决常见疑难问题
3.1 场景:Nginx 反向代理导致跨域失败
若服务端通过 Nginx 转发 WebSocket 请求,需确保代理配置正确:
nginx
location /ws/ { proxy_pass http://backend; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "Upgrade"; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 关键:传递 Origin 头 proxy_set_header Origin $http_origin; }
3.2 场景:Spring Security 拦截 WebSocket 握手
当项目集成 Spring Security 时,需显式放行 WebSocket 端点:
java
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/ws/**").permitAll() // 放行 WebSocket 端点 .anyRequest().authenticated(); } }
3.3 场景:SockJS 的跨域兼容性
使用 SockJS 时,浏览器可能发起 OPTIONS
预检请求,需确保 CORS 配置覆盖:
java
// 针对 /info 端点的预检请求 @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOrigins("https://client.com") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS"); }
四、安全实践:避免跨域配置中的陷阱
4.1 风险:滥用通配符 *
的后果
-
CSRF 攻击:恶意网站可通过 WebSocket 窃取会话信息。
-
数据泄露:未授权域名获取实时推送数据。
解决方案:
-
通过环境变量动态加载允许的域名列表。
-
结合 OAuth 2.0 在握手阶段验证 Token。
4.2 防御:WebSocket 层的身份验证
在握手拦截器中实现权限校验:
java
public boolean beforeHandshake(...) { String token = request.getHeaders().getFirst("Authorization"); if (!jwtService.validateToken(token)) { response.setStatusCode(HttpStatus.UNAUTHORIZED); return false; } return true; }
五、测试与验证
5.1 使用 Postman 测试跨域
Postman 支持手动设置 Origin
头:
-
新建 WebSocket 请求,URL 输入
wss://api.server.com/ws
-
在 Headers 中添加
Origin: https://unauthorized-site.com
-
观察连接是否被拒绝
5.2 浏览器端验证
在前端代码中捕获连接错误:
javascript
const socket = new WebSocket('wss://api.server.com/ws'); socket.onerror = (error) => { console.log('Connection failed:', error); };
六、总结
WebSocket 的跨域问题本质上是浏览器安全策略对握手阶段的约束。在 Spring Boot 中,通过合理配置 HandshakeInterceptor
、WebSocketConfigurer
或 WebSocketMessageBrokerConfigurer
,开发者可以灵活控制跨域行为。关键要点包括:
-
避免使用通配符,尽量动态验证 Origin。
-
区分 HTTP CORS 与 WebSocket 跨域,二者需独立配置。
-
结合安全框架,在握手阶段实现身份认证。
通过本文的实践方案,开发者不仅能解决跨域问题,还能构建更健壮的实时通信系统。
附录:常见问题速查表
问题现象 | 可能原因 | 解决方案 |
---|---|---|
连接返回 403 Forbidden | Origin 未在允许列表中 | 检查 setAllowedOrigins 配置 |
首次连接成功,重连失败 | 反向代理未保持 WebSocket 头 | 配置 Nginx 的 proxy_set_header |
前端控制台报 “Invalid UTF-8” | 未正确处理二进制消息 | 配置 BinaryType 或使用 STOMP |
掌握这些技巧后,WebSocket 跨域将不再是阻碍实时通信的绊脚石。