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

Spring Security集成StompJS/SockJS WebSocket时的CORS问题

WebSocket集成时的CORS错误解决方案

问题场景

为Web项目添加WebSocket功能时触发CORS错误,排查后怀疑是Spring Security与WebSocket配置规则冲突导致,相关代码如下:

WebSocketConfig

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket").setAllowedOrigins("*").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/message");
        config.setApplicationDestinationPrefixes("/app");
    }
}

Spring SecurityConfig

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final UserDetailsService userDetailsService;
    private final BCryptPasswordEncoder bCryptPasswordEncoder;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean());
        customAuthenticationFilter.setFilterProcessesUrl("/api/login");
        
        http.csrf().disable().cors().configurationSource(request -> {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
            configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE"));
            configuration.addAllowedHeader("*");
            return configuration;
        }).and()
        .authorizeRequests().anyRequest().permitAll().and()
        .addFilter(customAuthenticationFilter)
        .addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

CustomAuthorizationFilter

public class CustomAuthorizationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        if (request.getServletPath().equals("/api/login") || request.getServletPath().equals("/api/token/refresh")) {
            filterChain.doFilter(request, response);
        } else {
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                try {
                    String token = authorizationHeader.substring("Bearer ".length());
                    Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
                    JWTVerifier verifier = JWT.require(algorithm).build();
                    DecodedJWT decodedJWT = verifier.verify(token);

                    String username = decodedJWT.getSubject();
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    
                    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    Arrays.stream(roles).forEach(role -> {
                        authorities.add(new SimpleGrantedAuthority(role));
                    });

                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                            username, null, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                    filterChain.doFilter(request, response);
                } catch (Exception exception) {
                    log.error("Error logging in: {}", exception.getMessage());
                    response.setHeader("error", exception.getMessage());
                    response.setStatus(FORBIDDEN.value());

                    Map<String, String> error = new HashMap<>();
                    error.put("error_message", exception.getMessage());
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);

                    new ObjectMapper().writeValue(response.getOutputStream(), error);
                }
            } else {
                filterChain.doFilter(request, response);
            }

        }
    }
}

Angular端TokenInterceptor

@Injectable()
export class TokenInterceptor implements HttpInterceptor {

    constructor(private auth: AuthService, private router: Router) { }

    intercept(request: HttpRequest<unknown>, next: HttpHandler): Observable<HttpEvent<unknown>> | any {
        const myToken = this.auth.getToken();

        if (myToken) {
            request = request.clone({
                setHeaders: {
                    Authorization: `Bearer ${myToken}`
                },
            });
        }

        return next.handle(request).pipe(
            catchError((err: any) => {
                if (err instanceof HttpErrorResponse) {
                    if (err.status === 401) {
                        return this.handleUnAuthorizedError(request, next)
                    }
                }
                return throwError(() => new Error("Some other error occured"))
            })
        );
    }

    handleUnAuthorizedError(req: HttpRequest<any>, next: HttpHandler) {
        let tokenApiModel = new TokenApiModel();
        tokenApiModel.access_token = this.auth.getToken()!;
        tokenApiModel.refresh_token = this.auth.getRefreshToken()!;
        return this.auth.renewToken(tokenApiModel)
            .pipe(
                switchMap((data: TokenApiModel) => {
                    this.auth.storeRefreshToken(data.refresh_token)
                    this.auth.storeToken(data.access_token)
                    req = req.clone({
                        setHeaders: {
                            Authorization: `Bearer ${data.access_token}`
                        },
                        withCredentials: false
                    });
                    return next.handle(req)
                }),
                catchError((err) => {
                    return throwError(() => {
                        alert("Token is expired, login again")
                        this.router.navigate(["login"])
                    })
                })
            )
    }
}

Angular端SocketService

@Injectable({
  providedIn: 'root'
})
export class SocketService {
  public stompClient: any;
  public msg: Array<any> = [];

  constructor() {
    const serverUrl = 'http://localhost:8080/socket';
    const ws = new SockJS(serverUrl);
    this.stompClient = Stomp.over(ws);
  }

  initializeWebSocketConnection() {
    const that = this;
    console.log(this.stompClient); // It work until here 

    this.stompClient.connect({}, function (frame: any) {
      that.stompClient.subscribe('/message', (message: any) => {
        console.log(message);
        if (message.body) {

          that.msg.push(message.body);
        }
      });
      that.stompClient.subscribe('/messages', (message: any) => {
        console.log(message);
        if (message.body) {

          that.msg.push(message.body);
        }
      });
    });
  }

  sendMessage(message: any) {
    this.stompClient.send('/app/send/message', {}, message);
  }
}

解决方案

1. 统一CORS规则,避免WebSocket与Security配置不一致

WebSocketConfig中setAllowedOrigins("*")和Security里的允许源冲突,修改WebSocketConfig,将允许源和Security保持一致:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/socket")
                .setAllowedOrigins("http://localhost:4200") // 和Security配置匹配
                .withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/message");
        config.setApplicationDestinationPrefixes("/app");
    }
}

2. 更新Security配置,放行WebSocket请求与OPTIONS预检

SockJS握手前会发送OPTIONS预检请求,需要在Security中明确放行该方法,同时确保WebSocket端点不被拦截:

@Override
protected void configure(HttpSecurity http) throws Exception {
    CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(authenticationManagerBean());
    customAuthenticationFilter.setFilterProcessesUrl("/api/login");
    
    http.csrf().disable()
        .cors().configurationSource(request -> {
            CorsConfiguration configuration = new CorsConfiguration();
            configuration.setAllowedOrigins(Arrays.asList("http://localhost:4200"));
            configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); // 添加OPTIONS方法
            configuration.addAllowedHeader("*");
            configuration.setAllowCredentials(true); // 允许携带凭证(可选,根据需求)
            return configuration;
        }).and()
        .authorizeRequests()
        .antMatchers("/socket/**", "/api/login/**", "/api/token/refresh/**", "/api/user/save/**").permitAll() // 明确放行WebSocket端点
        .anyRequest().permitAll().and()
        .addFilter(customAuthenticationFilter)
        .addFilterBefore(new CustomAuthorizationFilter(), UsernamePasswordAuthenticationFilter.class);
}

3. 调整CustomAuthorizationFilter,跳过WebSocket请求

当前过滤器会拦截除登录、刷新token外的所有请求,包括WebSocket握手,需要添加跳过规则:

public class CustomAuthorizationFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String servletPath = request.getServletPath();
        // 跳过WebSocket、登录、刷新token的请求
        if (servletPath.equals("/api/login") 
            || servletPath.equals("/api/token/refresh")
            || servletPath.startsWith("/socket/")) {
            filterChain.doFilter(request, response);
        } else {
            // 原有认证逻辑保持不变
            String authorizationHeader = request.getHeader(AUTHORIZATION);
            if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
                try {
                    String token = authorizationHeader.substring("Bearer ".length());
                    Algorithm algorithm = Algorithm.HMAC256("secret".getBytes());
                    JWTVerifier verifier = JWT.require(algorithm).build();
                    DecodedJWT decodedJWT = verifier.verify(token);

                    String username = decodedJWT.getSubject();
                    String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
                    
                    Collection<SimpleGrantedAuthority> authorities = new ArrayList<>();
                    Arrays.stream(roles).forEach(role -> {
                        authorities.add(new SimpleGrantedAuthority(role));
                    });

                    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                            username, null, authorities);
                    SecurityContextHolder.getContext().setAuthentication(authenticationToken);

                    filterChain.doFilter(request, response);
                } catch (Exception exception) {
                    log.error("Error logging in: {}", exception.getMessage());
                    response.setHeader("error", exception.getMessage());
                    response.setStatus(FORBIDDEN.value());

                    Map<String, String> error = new HashMap<>();
                    error.put("error_message", exception.getMessage());
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);

                    new ObjectMapper().writeValue(response.getOutputStream(), error);
                }
            } else {
                filterChain.doFilter(request, response);
            }
        }
    }
}

4. Angular端SocketService优化(可选)

添加错误回调方便排查问题,同时根据后端配置开启凭证携带:

@Injectable({
  providedIn: 'root'
})
export class SocketService {
  public stompClient: any;
  public msg: Array<any> = [];

  constructor() {
    const serverUrl = 'http://localhost:8080/socket';
    const ws = new SockJS(serverUrl, null, {
      withCredentials: true // 后端允许的话开启
    });
    this.stompClient = Stomp.over(ws);
  }

  initializeWebSocketConnection() {
    const that = this;
    this.stompClient.connect({}, 
      function (frame: any) {
        that.stompClient.subscribe('/message', (message: any) => {
          console.log(message);
          if (message.body) {
            that.msg.push(message.body);
          }
        });
        that.stompClient.subscribe('/messages', (message: any) => {
          console.log(message);
          if (message.body) {
            that.msg.push(message.body);
          }
        });
      },
      function(error: any) {
        console.error('WebSocket连接失败:', error); // 添加错误回调
      });
  }

  sendMessage(message: any) {
    this.stompClient.send('/app/send/message', {}, message);
  }
}

内容的提问来源于stack exchange,提问作者Tuan Le Anh

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 16:09:56