You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring Boot 3 WebSocket无法通过JWT认证问题求助

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升级请求,需要确保过滤器能覆盖到这类请求。

修改步骤:

  1. 调整过滤器的拦截范围
    重写OncePerRequestFilter的shouldNotFilter方法,确保WebSocket握手请求不会被过滤掉:

    @Override
    protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
        // 只排除非WebSocket端点的请求,根据你的实际端点调整路径
        return !request.getRequestURI().equals("/my-websocket-endpoint");
    }
    
  2. 修复查询参数提取逻辑
    当前代码只处理了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;
            }
        }
    }
    
  3. 传递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握手请求,然后在连接建立后通过自定义消息完成认证。

修改步骤:

  1. 修改SecurityFilterChain,放行WebSocket端点
    在authorizeHttpRequests中添加WebSocket端点的放行规则:

    .requestMatchers("/login", "/my-websocket-endpoint")
        .permitAll()
    
  2. 实现带认证逻辑的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);
        }
    }
    
  3. 修改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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.29 04:44:57