javaspring-bootwebsocketcorssockjs

Java Springboot: CORS Missing Allow Origin error but CORS configuration is actually defined


I am trying to set up a simplistic WebSocket Server using Java Springboot and although I have defined CORS configuration (and STOMP for SockJS) to allow origins from localhost:8080 (and previously with star sign) I still see that 'Access-Control-Allow-Origin' is missing from the Headers and I get the 403 status code error.

From the clientside I have a simple SockJS setup to connect to the Websocket normally via plain HTML and JS. Here is my Java Code (note the multiple attempts I had defining CORS allow origins)...

Websocket Broker Configuration:

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
  @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
      registry.enableSimpleBroker("/topic");
      registry.setApplicationDestinationPrefixes("/app");
    }
  
  @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
      registry.addEndpoint("/stomp")
        .setAllowedOrigins("http://localhost:8080")
        .setAllowedOriginPatterns("*")
        .withSockJS();
    }
}

And another class where I define CORS configuration and Security rules of Springboot (for a lack of better name at this time I called this class WebSocketSecurity):

@Configuration
@EnableWebSecurity
public class WebSocketSecurity {
  private CorsConfiguration corsConfiguration() {
    CorsConfiguration corsConfig = new CorsConfiguration();
    corsConfig.addAllowedOrigin("http://localhost:8080");
    corsConfig.addAllowedHeader("*");
    corsConfig.addAllowedMethod(HttpMethod.GET);
    corsConfig.addAllowedMethod(HttpMethod.POST);
    corsConfig.applyPermitDefaultValues();
    return corsConfig;
  }

  @Bean
  public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.cors().configurationSource(request -> corsConfiguration());
    http.headers().frameOptions().sameOrigin();
    return http.build();
  }
  
  public WebSecurityCustomizer webSecurityCustomizer() {
    return (web) -> web.ignoring().antMatchers("/images/**", "/js/**");
  }
  
  @Bean
  public WebMvcConfigurer corsConfigurer() {
    return new WebMvcConfigurer() {
      @Override
      public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
          .allowedOrigins("http://localhost:8080")
          .allowedMethods("GET", "POST", "PUT", "PATCH", "DELETE", "HEAD")
          .allowedHeaders("*");
          //.allowCredentials(true); // this is disabled if CORS allow origin is set to "*"
      }
    };
  }
  
  @Bean
  public CookieSameSiteSupplier cookieSameSiteSupplier(){
    return CookieSameSiteSupplier.ofNone();
  }
}

My pom.xml has these dependencies and build configuration:

    <properties>
        <java.version>17</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-websocket</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-reactor-netty</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${project.parent.version}</version>
            </plugin>
        </plugins>
    </build>

Main launch class for Springboot:

@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class WSExampleApplication {

    public static void main(String[] args) {
        SpringApplication.run(WSExampleApplication.class, args);
    }

}

And here is the SockJS part I use in the Frontend:

function connect(event) {
    username = document.querySelector('#name').value.trim();
    if(username) {
        var socket = new SockJS('http://localhost:8080/stomp');
        stompClient = Stomp.over(socket);
        stompClient.connect({}, onConnected, onError);
    }
    event.preventDefault();
}


function onConnected() {
    stompClient.subscribe('/topic/public', onMessageReceived); // Subscribe to the Public Topic
    stompClient.send("/app/chat.register", // Tell your username to the server
        {},
        JSON.stringify({sender: username, type: 'JOIN'})
    )
}


function onError(error) {
    connectingElement.textContent = 'Could not connect to WebSocket server!';
    connectingElement.style.color = 'red';
}

CORS error in browser console CORS error in browser network tab

Is there an issue with the CORS/Websocket configuration or is there something I need to change on the Frontend with SockJS?

I am on Spring Boot version 2.7.4 and Java version 18.0.2.1 and I use Tomcat.


Solution

  • For lack of information about how the frontend is served, this could go one of three ways. I will address each below.


    BE and FE are separate web servers, listening on different localhost ports

    I assume the backend is listening on port 8080, since that's where you try to connect to in the frontend code.

    In the CORS configuration, you need to allow the origin that corresponds to the frontend. I got the websocket connection to work with the following modifications (for the FE, I used a simple React app on port 5000):

    WebSocketConfiguration.java

    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
        @Override
        public void configureMessageBroker(MessageBrokerRegistry registry) {
            registry.enableSimpleBroker("/topic");
            registry.setApplicationDestinationPrefixes("/app");
        }
    
        @Override
        public void registerStompEndpoints(StompEndpointRegistry registry) {
            registry.addEndpoint("/stomp")
                .setAllowedOrigins("http://localhost:5000")
                .withSockJS();
        }
    }
    

    SecurityConfiguration.java (was WebSocketSecurity in your post)

    @Configuration
    @EnableWebSecurity
    public class SecurityConfiguration {
        @Bean
        @Primary
        public CorsConfigurationSource corsConfiguration() {
            CorsConfiguration corsConfig = new CorsConfiguration();
            corsConfig.setAllowedOrigins(List.of("http://localhost:5000"));
            corsConfig.setAllowedMethods(List.of("GET", "POST"));
            corsConfig.setAllowCredentials(true);
    
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", corsConfig);
            return source;
        }
    
        @Bean
        public SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource configurationSource) throws Exception {
            return http
                .cors().configurationSource(configurationSource)
                .and()
                .headers().frameOptions().sameOrigin()
                .and()
                .build();
        }
    
        @Bean
        public CookieSameSiteSupplier cookieSameSiteSupplier(){
            return CookieSameSiteSupplier.ofNone();
        }
    }
    

    Ideally, the list of origins to allow (along with all other cors configuration parameters) should come from custom properties so as to make them uniform across web and websocket configs instead of string literals.


    FE is served through the BE web server

    If you're serving the frontend application through the backend (static content in resources), then CORS should not be an issue at all, since both the frontend and the backend will have the same base uri. In the FE, just refer to the backend without it, i.e. simply as /stomp.


    Opening FE index.html directly in the browser by filesystem path

    If you're testing the frontend without a web server (i.e. just opening html file directly in the browser through a filesystem path), then this is not the best position to be in when it comes to CORS. I strongly recommend that you spin up a web server for the FE too (or serve it through the BE), even for local testing - and that takes you back to the first two sections of my answer.