spring security 默认不拦截 websocket 握手请求,也不自动校验 token,必须显式配置 web.ignoring().antmatchers("/ws/**") 绕过整个过滤器链,并通过 handshakeinterceptor 或 sec-websocket-protocol 在握手阶段安全传递和校验 token,否则连接将 403 或绕过认证。

Spring Security 默认不拦截 WebSocket 握手请求,但也不自动校验 Token —— 你必须显式配置放行路径 + 自定义解析逻辑,否则连接直接 403 或绕过认证。
为什么 /ws/** 路径放行后仍报 403?
因为 Spring Security 的 HttpSecurity 配置只控制“是否允许访问”,不控制“是否跳过过滤器链”。WebSocket 握手请求(如 GET /ws/chat)虽匹配 .antMatchers("/ws/**").permitAll(),但仍会经过 JwtTokenFilter 等自定义过滤器。而这些过滤器通常只从 Authorization 请求头或 Cookie 读 Token,但 WebSocket 握手请求默认不携带这些字段。
常见错误现象:
- 浏览器 Network 面板看到
/ws/chat/info(SockJS)或/ws/chat(原生)返回 403 - 日志里没进你的
JwtTokenFilter.doFilterInternal(),说明过滤器根本没被触发 —— 实际是被更早的 Spring Security 内置检查拦下了
真正该做的是在 WebSecurity 层彻底忽略握手端点:
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers(
"/ws/**",
"/topic/**",
"/app/**",
"/user/**"
);
}
注意:web.ignoring() 和 http.permitAll() 完全不同:前者让请求绕过整个 Spring Security 过滤器链;后者仍走链,只是授权放行。
如何在握手阶段安全传递并校验 Token?
不能依赖 Authorization 头(浏览器限制、SockJS 不支持),也不能拼在 URL 里(日志泄露、长度受限)。唯一可靠方式是利用 WebSocket 协议标准字段:Sec-WebSocket-Protocol。
前端建立连接时传入:
const socket = new WebSocket( "wss://api.example.com/ws/chat", ["Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."] );
后端在 ServerEndpointConfig.Configurator 中提取:
@Override
public void modifyHandshake(ServerEndpointConfig sec, HandshakeRequest request, HandshakeResponse response) {
List<string> protocols = request.getHeaders().get("Sec-WebSocket-Protocol");
if (protocols != null && !protocols.isEmpty()) {
String token = protocols.get(0).replace("Bearer ", "");
// 解析 JWT、查 Redis、调 OAuth2 /check_token...
if (!isValidToken(token)) {
throw new RuntimeException("Invalid token");
}
sec.getUserProperties().put("userId", extractUserId(token));
}
}</string>
关键点:
- 子协议值必须是合法字符串,JWT 中若含
+或/可能被 URL 编码破坏,建议前端先encodeURIComponent(token),后端再URLDecoder.decode() - 不要在
@OnOpen里校验 —— 此时连接已建立,拒绝只能靠session.close(),用户体验差且易被重连绕过 - 务必用
wss://,明文传输 Token 极其危险
Stomp over WebSocket 场景下 Token 怎么办?
如果你用的是 @EnableWebSocketMessageBroker + STOMP(比如 SockJS),那握手流程多一层:GET /ws/stomp/info → POST /ws/stomp/xxx → 建立 STOMP 会话。这个 /info 请求是 HTTP GET,无法带自定义头,也无法用子协议。
此时必须改用 URL 参数传 Token,并配合 WebSecurity.ignoring() 放行整个端点前缀:
// 前端
const socket = new SockJS("https://api.example.com/ws/stomp?token=eyJhbG...");
const stompClient = Stomp.over(socket);
// 后端配置
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/ws/stomp/**");
}
然后在 HandshakeInterceptor 中取参:
public class JwtHandshakeInterceptor implements HandshakeInterceptor {
@Override
public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response,
WebSocketHandler wsHandler, Map<string object> attributes) throws Exception {
if (request.getQueryString() != null) {
MultiValueMap<string string> params = ServletUriComponentsBuilder.fromHttpRequest(request)
.build().getQueryParams();
String token = params.getFirst("token");
if (isValidToken(token)) {
attributes.put("userId", extractUserId(token));
return true;
}
}
return false; // 拒绝握手
}
}</string></string>
注意:beforeHandshake 返回 false 会直接中断连接,比在 @MessageMapping 里校验更早、更安全。
为什么不能只靠 Session 或 Cookie 校验?
因为 WebSocket 握手请求可能不携带 Cookie(跨域、禁用第三方 Cookie、某些移动端 WebView),而且 Session ID 本身不是认证凭证,它只是服务端状态标识。一旦攻击者拿到有效 Session ID(比如通过 XSS),就能复用连接。
真实项目中容易被忽略的点:
- 没验证 Token 签名和过期时间,仅 base64 解码就信任内容
- 把用户角色存进
session.getAttributes(),但后续消息处理时没从该 session 取,而是重新查数据库 —— 白费握手校验 - 用了
Principal对象,却没在StompHeaderAccessor或Session中正确绑定,导致@PreAuthorize("hasRole('ADMIN')")失效
最稳妥的做法:握手时解析 Token 并存入 session.getUserProperties() 或 attributes,后续所有消息处理都基于这个上下文,不再重复解析或远程校验。











