4

I've got an extremely simple example that I can't get to work.

I have my domain that models my database, and my Repository.

public interface MyTestRepository extends CrudRepository<MyTest, Integer> {
}

I used http://resttesttest.com/ to test it. For GET Method's it returns me the JSON REST information without any issue.

I can query the endpoint http://localhost:8080/mytest/1 and I get back the information for id=1 from the database.

However, the problem comes in when I try to use the DELETE option. If I run a DELETE on http://localhost:8080/mytest/1 I get

Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://resttesttest.com' is therefore not allowed access. The response had HTTP status code 403.

I initially tried the following, but found out that I can't use it because I'm using Spring-data-Rest. https://jira.spring.io/browse/DATAREST-573

@Override
public void addCorsMappings(CorsRegistry registry) {
    registry.addMapping("/**")
        .allowedOrigins("*")
        .allowedMethods("*")
        .allowedHeaders("*")
        .allowCredentials(true).maxAge(3600);
}

I googled around and found this.

How to configure CORS in a Spring Boot + Spring Security application?

So I added

@Bean
public FilterRegistrationBean corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("*");
    source.registerCorsConfiguration("/**", config);
    FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
    bean.setOrder(0);
    return bean;
}

I also found this thread.

Spring Data Rest and Cors

and tried the following code as well, but no luck.

@Bean
public FilterRegistrationBean corsFilter() {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("OPTIONS");
    config.addAllowedMethod("HEAD");
    config.addAllowedMethod("GET");
    config.addAllowedMethod("PUT");
    config.addAllowedMethod("POST");
    config.addAllowedMethod("DELETE");
    config.addAllowedMethod("PATCH");
    source.registerCorsConfiguration("/**", config);
    // return new CorsFilter(source);
    final FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
    bean.setOrder(0);
    return bean;
}

I added a catch all to test which should allow everything CORS wise to pass, however I still keep getting the No 'Access-Control-Allow-Origin' even though I have "*".

At this point I have no idea what I am missing on why the preflight request doesn't pass access control check.

curl has no problem issuing the delete.

Edit:

Ended up finding the exact solution. I'm not sure of the differences between what I have and this method, but this seems to work.

import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * Note this is a very simple CORS filter that is wide open.
 * This would need to be locked down.
 * Source: https://stackoverflow.com/questions/39565438/no-access-control-allow-origin-error-with-spring-restful-hosted-in-pivotal-web
 */
@Component
public class CORSFilter implements Filter {

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
        response.setHeader("Access-Control-Max-Age", "3600");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
        chain.doFilter(req, res);
    }

    public void init(FilterConfig filterConfig) {}

    public void destroy() {}

}
Community
  • 1
  • 1
Kevin Vasko
  • 1,561
  • 3
  • 22
  • 45
  • When using straight Spring Boot MVC/Rest without using spring-boot-starter-security, I had to use the above CORSFIlter to allow the preflighted OPTIONS to work. Without the filter when I used Postman to send in an OPTIONS request with the Origin header I got a 403 Forbidden (OPTIONS would work fine if I left Origin off). I needed both the "public class CORSFilter implements Filter" and a "public class CORSconfiguration implements WebMvcConfigurer" to get OPTIONS to work properly – LeslieM Jun 30 '20 at 19:49

4 Answers4

3

The following configuration works for me in a Spring Data Rest based application. The important point to note is that the filter is registered to execute before the Security Filter chain kicks in.

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter
{
  @Override
  public void configure(HttpSecurity http) throws Exception
  {
    http.addFilterBefore(corsFilter(), ChannelProcessingFilter.class);
  }

  @Bean
  protected Filter corsFilter()
  {
    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

    CorsConfiguration config = new CorsConfiguration();
    config.setAllowCredentials(true);
    config.addAllowedOrigin("*");
    config.addAllowedHeader("*");
    config.addAllowedMethod("OPTIONS");
    config.addAllowedMethod("HEAD");
    config.addAllowedMethod("GET");
    config.addAllowedMethod("PUT");
    config.addAllowedMethod("POST");
    config.addAllowedMethod("DELETE");
    config.addAllowedMethod("PATCH");
    config.addExposedHeader("Location");

    source.registerCorsConfiguration("/**", config);

    return new CorsFilter(source);
  }
}
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • Thanks, but that doesn't seem to work either. I copy and pasted that directly into my Spring App, disabled all the other @Configuration classes added the imports and still got the same errors. Are you using Springs Boot Data Rest? If so are you using the Repository class that generates the REST endpoints automatically? – Kevin Vasko Oct 13 '16 at 17:51
2

This is what I use as a permit all CORS servlet filter:

public class PermissiveCORSFilter implements Filter {

    private static final Logger LOGGER = LoggerFactory.getLogger(PermissiveCORSFilter.class);
    private static final Pattern PATTERN = Pattern.compile("^[a-zA-Z0-9 ,-_]*$");

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) res;
        HttpServletRequest request = (HttpServletRequest) req;

        String origin;
        String credentialFlag;
        if (request.getHeader("Origin") == null) {
            origin = "*";
            credentialFlag = "false";
         } else {
            origin = request.getHeader("Origin");
            credentialFlag = "true";
         }

        // need to do origin.toString() to avoid findbugs error about response splitting        
        response.addHeader("Access-Control-Allow-Origin", origin.toString());
        response.setHeader("Access-Control-Allow-Credentials", credentialFlag);
        if ("OPTIONS".equals(request.getMethod())) {
            LOGGER.info("Received OPTIONS request from origin:" + request.getHeader("Origin"));
            response.setHeader("Access-Control-Allow-Methods", "GET,POST,HEAD,OPTIONS,PUT,DELETE");
            response.setHeader("Access-Control-Max-Age", "3600");
            String headers = StringUtils.trimToEmpty(request.getHeader("Access-Control-Request-Headers"));
            if (!PATTERN.matcher(headers).matches()) {
                throw new ServletException("Invalid value provided for 'Access-Control-Request-Headers' header");
            }
            response.setHeader("Access-Control-Allow-Headers", headers); // allow any headers
        }
        chain.doFilter(req, res);
    }

    @Override
    public void init(FilterConfig filterConfig) {
        // Do nothing
    }

    @Override
    public void destroy() {
        // Do nothing
    }
Pete
  • 1,500
  • 2
  • 16
  • 33
  • Thanks, but based on that bug in my OP that won't work for me since I'm using Spring Data REST – Kevin Vasko Oct 13 '16 at 02:50
  • ok, that's interesting.. we're using spring mvc + spring data and that filter seems to work fine when being called from our angular web clients – Pete Oct 13 '16 at 05:55
  • Are you using the automatic generated repositories? e.g. Repository() {}? My classes that I marked with @CrossOrigin(origins = "*") to test do not seem to have this problem. – Kevin Vasko Oct 13 '16 at 12:54
  • Ok I stand corrected. I tried to use your code but didn't seem to work. I ended up finding this http://stackoverflow.com/a/39573047/3314194, copy and pasted that code into a class and it worked. It is the same concept as yours. I'll mark yours as an answer but I'm going to add an edit to my question that is the complete code that works for me (feel free to add it to your answer). I still don't understand the difference between the 3 different CORS filters though. – Kevin Vasko Oct 13 '16 at 20:20
  • Thanks :) Yeah it does look similar, It might have something to do with the "Access-Control-Allow-Methods" needing to explicitly allow DELETE – Pete Oct 13 '16 at 22:48
0

Using Spring Boot 2.2.6

I had to add a filter to allow OPTIONS to work. Without it, I got a 403 Forbidden. The "Origin" request header is what triggered the 403 - I tested in Postman and without sending that header OPTIONS worked without a filter.

import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
@Component
            
        public class CORSFilter implements Filter {
                
                public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
                    HttpServletResponse response = (HttpServletResponse) res;
                    response.setHeader("Access-Control-Allow-Origin", "*");
                    response.setHeader("Access-Control-Allow-Methods", "OPTIONS");  // "POST, GET, PUT, OPTIONS, DELETE"
                    response.setHeader("Access-Control-Max-Age", "3600");
                    response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
                    chain.doFilter(req, res);
                }
        
            public void init(FilterConfig filterConfig) {}
        
            public void destroy() {}
        
        }

Along with

@Configuration
public class ConfigCORS implements WebMvcConfigurer {
@Override
    public void addCorsMappings(CorsRegistry registry) {

        registry.addMapping("/**")  
                .allowedOrigins("*")  allowedOrigins("http://localhost:3000")
                .allowedMethods("POST", "PUT", "GET",  "DELETE", "OPTIONS") 
                .allowedHeaders("Content-Type", "Origin")
                .exposedHeaders("X-Total-Count", "Location", "Access-Control-Allow-Origin")  
                .allowCredentials(false)
                .maxAge(6000);
    }
}
LeslieM
  • 2,105
  • 1
  • 17
  • 8
0

I seem to have the same issue. CrossOrigin config works fine with GET/PUT/POST, but when I request OPTIONS for my Spring PostMapping method the response omits the Access-Control-Allow-Methods header:

@CrossOrigin
public class ArticleController {

@DeleteMapping("/{uuid}")
public void delete(@PathVariable String uuid) throws ArticleNotFoundException {
    articleService.delete(uuid);
}

If I curl for DELETE, I get a HTTP 200 including Access-Control-Allow-Methods:

$ curl -v -H "Access-Control-Request-Method: DELETE" -H "Origin: http://localhost:4200" -X OPTIONS http://localhost:8080/article/someuuid
< HTTP/1.1 200
< Access-Control-Allow-Origin: http://localhost:4200
< Access-Control-Allow-Methods: PUT,POST,GET,DELETE,OPTIONS
< Allow: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH

If I curl for OPTIONS, I get a 403:

$ curl -v -H "Access-Control-Request-Method: OPTIONS" -H "Origin: http://localhost:4200" -X OPTIONS http://localhost:8080/article/someuuid
< HTTP/1.1 403

Am I missing something here?

EDIT 1:

If I add this mapping to the controller (based on Enable CORS for OPTIONS request using Spring Framework ):

@RequestMapping(
        value = "/**",
        method = RequestMethod.OPTIONS
)
public ResponseEntity handle() {
    return new ResponseEntity(HttpStatus.OK);
}

This results in:

$ curl -v -H "Access-Control-Request-Method: OPTIONS" -H "Origin: http://localhost:4200" -X OPTIONS http://localhost:8080/article/someuuid
< HTTP/1.1 200
< Access-Control-Allow-Origin: *
< Access-Control-Allow-Methods: OPTIONS
< Allow: GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH

But it doesn't solve the issue for Angular which still gives a 403

EDIT 2: I've been able to fix this with the following Controller-code:

@RequestMapping("/article")
@CrossOrigin(origins="http://localhost:4200",
    methods = {RequestMethod.PUT, RequestMethod.POST, RequestMethod.GET, RequestMethod.DELETE, RequestMethod.OPTIONS}
    )
public class ArticleController {

@RequestMapping(
        value = "/{uuid}",
        method = { RequestMethod.DELETE })
public void delete(@PathVariable String uuid) throws ArticleNotFoundException {
    articleService.delete(uuid);
}

@RequestMapping(method = { RequestMethod.OPTIONS})
public ResponseEntity handle() {
    return new ResponseEntity(HttpStatus.OK);
}
BramP
  • 51
  • 6