Spring Boot 3 WebSocket无法通过JWT认证问题求助
问题背景
我有一个Spring Boot 3应用,已经为HTTP请求实现了JWT认证。现在要集成WebSocket,但遇到了认证问题:Postman里给WebSocket请求加Bearer请求头能正常连接,但Angular端无法给WebSocket请求添加Authorization头,导致连接失败。
我尝试在Angular里把token放到URL查询参数中:
this.socket = new WebSocket('ws://localhost:9090/my-websocket-endpoint?token=' + localStorage.getItem("jwt"));
同时修改了JwtAuthenticationFilter的doFilterInternal方法,尝试从查询参数中提取token,并且能看到请求的token确实经过了这个方法,但认证始终不生效。相关的过滤器和Security配置代码如下:
自定义JWT过滤器代码
@Component @RequiredArgsConstructor public class JwtAuthenticationFilter extends OncePerRequestFilter { Logger LOG = LoggerFactory.getLogger(JwtAuthenticationFilter.class); private final JwtService jwtService; private final UserDetailsService userDetailsService; private final TokenRepository tokenRepository; @Override protected void doFilterInternal( @NonNull HttpServletRequest request, @NonNull HttpServletResponse response, @NonNull FilterChain filterChain ) throws ServletException, IOException { String query = request.getQueryString(); boolean jwtInQuery = false; if (query != null && query.startsWith("token")) { System.out.println("Token: " + query.substring(7)); jwtInQuery = true; } final String authHeader = request.getHeader("Authorization"); final String jwt; final String userEmail; if ((authHeader == null ||!authHeader.startsWith("Bearer ")) && !jwtInQuery) { filterChain.doFilter(request, response); return; } if (jwtInQuery) { jwt = query.substring(7); } else { jwt = authHeader.substring(7); } userEmail = jwtService.extractUsername(jwt); if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail); var isTokenValid = tokenRepository.findByToken(jwt) .map(t -> !t.isExpired() && !t.isRevoked()) .orElse(false); if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) { UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities() ); authToken.setDetails( new WebAuthenticationDetailsSource().buildDetails(request) ); SecurityContextHolder.getContext().setAuthentication(authToken); } } filterChain.doFilter(request, response); } }
SecurityFilterChain配置代码
package com.threatsoft.vulnslayer.config; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutHandler; import org.springframework.web.socket.config.annotation.EnableWebSocket; @Configuration @EnableWebSecurity @RequiredArgsConstructor @EnableWebSocket public class SecurityConfiguration { private final JwtAuthenticationFilter jwtAuthFilter; private final AuthenticationProvider authenticationProvider; private final LogoutHandler logoutHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf() .disable() .authorizeHttpRequests() .requestMatchers("/login") .permitAll() .anyRequest() .authenticated() .and() .sessionManagement() .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authenticationProvider(authenticationProvider) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .logout() .logoutUrl("/api/v1/auth/logout") .addLogoutHandler(logoutHandler) .logoutSuccessHandler((request, response, authentication) -> SecurityContextHolder.clearContext()); return http.build(); } }
解决方案一:通过现有过滤器完成WebSocket认证
你的过滤器逻辑本身没问题,但WebSocket握手请求的处理有个关键点:Spring Security的OncePerRequestFilter默认不会拦截WebSocket的握手请求,因为WebSocket握手属于HTTP升级请求,需要确保过滤器能覆盖到这类请求。
修改步骤:
调整过滤器的拦截范围
重写OncePerRequestFilter的shouldNotFilter方法,确保WebSocket握手请求不会被过滤掉:@Override protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException { // 只排除非WebSocket端点的请求,根据你的实际端点调整路径 return !request.getRequestURI().equals("/my-websocket-endpoint"); }修复查询参数提取逻辑
当前代码只处理了query.startsWith("token")的情况,若URL有多个查询参数会导致token解析失败,替换为健壮的提取方式:String jwt = null; boolean jwtInQuery = false; String query = request.getQueryString(); if (query != null) { String[] params = query.split("&"); for (String param : params) { if (param.startsWith("token=")) { jwt = param.substring(6); jwtInQuery = true; break; } } }传递SecurityContext到WebSocket会话
WebSocket握手成功后,需要手动把认证信息存入会话属性,后续消息处理时才能获取:@Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(myWebSocketHandler(), "/my-websocket-endpoint") .addInterceptors(new HandshakeInterceptor() { @Override public boolean beforeHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Map<String, Object> attributes) throws Exception { // 从SecurityContext取出认证信息,存入WebSocket会话 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null) { attributes.put("authenticatedUser", auth.getPrincipal()); } return true; } @Override public void afterHandshake(ServerHttpRequest request, ServerHttpResponse response, WebSocketHandler wsHandler, Exception exception) {} }) .setAllowedOrigins("*"); // 根据实际业务配置跨域规则 } @Bean public WebSocketHandler myWebSocketHandler() { return new MyWebSocketHandler(); } }在WebSocketHandler中获取认证信息:
public class MyWebSocketHandler extends TextWebSocketHandler { @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { UserDetails user = (UserDetails) session.getAttributes().get("authenticatedUser"); // 使用用户信息处理业务消息 } }
解决方案二:排除WebSocket认证,握手后自定义认证
如果不想通过HTTP过滤器处理WebSocket认证,可以先放行WebSocket握手请求,然后在连接建立后通过自定义消息完成认证。
修改步骤:
修改SecurityFilterChain,放行WebSocket端点
在authorizeHttpRequests中添加WebSocket端点的放行规则:.requestMatchers("/login", "/my-websocket-endpoint") .permitAll()实现带认证逻辑的WebSocketHandler
连接建立后要求客户端发送JWT完成认证,验证通过后再处理业务消息:@Component public class AuthRequiredWebSocketHandler extends TextWebSocketHandler { private final JwtService jwtService; private final UserDetailsService userDetailsService; private final TokenRepository tokenRepository; // 存储已认证的会话 private final Map<WebSocketSession, UserDetails> authenticatedSessions = new ConcurrentHashMap<>(); public AuthRequiredWebSocketHandler(JwtService jwtService, UserDetailsService userDetailsService, TokenRepository tokenRepository) { this.jwtService = jwtService; this.userDetailsService = userDetailsService; this.tokenRepository = tokenRepository; } @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { // 连接建立后发送认证提示 session.sendMessage(new TextMessage("请发送认证消息,格式:{\"action\":\"auth\",\"token\":\"你的JWT\"}")); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { if (!authenticatedSessions.containsKey(session)) { // 处理认证请求 try { JSONObject authMsg = new JSONObject(message.getPayload()); if ("auth".equals(authMsg.getString("action"))) { String jwt = authMsg.getString("token"); String userEmail = jwtService.extractUsername(jwt); UserDetails userDetails = userDetailsService.loadUserByUsername(userEmail); var isTokenValid = tokenRepository.findByToken(jwt) .map(t -> !t.isExpired() && !t.isRevoked()) .orElse(false); if (jwtService.isTokenValid(jwt, userDetails) && isTokenValid) { authenticatedSessions.put(session, userDetails); session.sendMessage(new TextMessage("认证成功")); } else { session.sendMessage(new TextMessage("无效的token")); session.close(); } } else { session.sendMessage(new TextMessage("请先完成认证")); } } catch (JSONException e) { session.sendMessage(new TextMessage("认证消息格式错误")); session.close(); } } else { // 已认证,处理业务消息 UserDetails user = authenticatedSessions.get(session); session.sendMessage(new TextMessage("收到消息:" + message.getPayload() + ",当前用户:" + user.getUsername())); } } @Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { authenticatedSessions.remove(session); super.afterConnectionClosed(session, status); } }修改Angular端代码,先连接再发送认证消息
connect() { this.socket = new WebSocket('ws://localhost:9090/my-websocket-endpoint'); this.socket.onopen = () => { console.log('WebSocket连接已建立'); // 发送认证消息 const authMsg = { action: 'auth', token: localStorage.getItem("jwt") }; this.socket.send(JSON.stringify(authMsg)); }; this.socket.onmessage = (event) => { console.log('收到服务器消息:', event.data); // 处理返回消息 }; this.socket.onerror = (error) => { console.error('WebSocket连接错误:', error); }; this.socket.onclose = (event) => { console.log('WebSocket连接关闭:', event); }; }
内容的提问来源于stack exchange,提问作者thanos denkseropoulos

