简体   繁体   中英

Spring Boot: CORS working with GET request but not POST request

Problem:

CORS is working with GET requests that do not have @AuthenticatedPrincipal in the method parameter.

I'm using

  • OKTA as my authentication server.
  • Spring REST controllers
  • Spring Data (CrudRepository)

For some reason, I'm getting this error everytime, I am making a POST, PUT or unsafe request that has @AuthenticatedPrincipal in the method parameter, but it is working for GET requests:

Error code

I have read every possible Stackoverflow question on CORS settings with Spring Boot and configured my CORS like this but to no avail:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
                .csrf().disable()
//                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
//                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers( "/courses", "/testing/*").permitAll()
                .anyRequest().authenticated().and()
                .oauth2Login().and()
                .oauth2ResourceServer().jwt();
        http.headers().frameOptions().disable();
    }

//    @Bean
//    CorsConfigurationSource corsConfigurationSource() {
//        CorsConfiguration configuration = new CorsConfiguration();
//        configuration.setAllowedOrigins(Arrays.asList("*"));
//        configuration.setAllowedMethods(Arrays.asList("POST", "GET", "PUT", "OPTIONS", "HEAD", "DELETE"));
//        configuration.setAllowedHeaders(Arrays.asList("Content-Type"));
//        configuration.setExposedHeaders(Arrays.asList("Authorization", "Link", "X-Total-Count", "Access-Control-Allow-Headers"));
//        configuration.setAllowCredentials(true);
//        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//        source.registerCorsConfiguration("/**", configuration);
//        return source;
//    }

    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration().applyPermitDefaultValues();
        configuration.setAllowedOrigins(ImmutableList.of("*"));
        configuration.setAllowedMethods(ImmutableList.of("OPTIONS", "HEAD", "GET", "POST", "PUT", "DELETE", "PATCH"));
        // setAllowCredentials(true) is important, otherwise:
        // The value of the 'Access-Control-Allow-Origin' header in the response must not be the wildcard '*' when the
        // request's credentials mode is 'include'.
        configuration.setAllowCredentials(true);
        // setAllowedHeaders is important! Without it, OPTIONS preflight request
        // will fail with 403 Invalid CORS request
        configuration.setAllowedHeaders(ImmutableList.of("Authorization", "Cache-Control", "Content-Type"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }


My Controllers

@CrossOrigin
@RestController
@RequestMapping(EndPoints.TEST_PATH)
public class TestController {

    private final TestService tService;

    @Autowired
    public TestController(TestService tService) {
        this.tService = tService;
    }

    @PostMapping(path="/create")
    public ResponseEntity<String> addNewTest(@Valid @NonNull @RequestBody TestDTO data,
                                               @AuthenticationPrincipal Jwt principal){

        tokenUserEmail =  token.getClaimAsString("sub");
        tokenUid = token.getClaimAsString("uid");
        tokenFullName = token.getClaimAsString("firstName") + " "
                + token.getClaimAsString("lastName");
        
        TestEntity testEntity = new TestEntity();
        testEntity.setName(tokenFullName);
        testEntity.setGrade(data.grade);
        testEntity.setNumber(data.number);

        tService.addNewTest(testEntity);
        
    }
    

}

I have exhausted every single possible Stackoverflow question, created WebMvcConfigurerAdapter() , set and unset @CrossOrigin . At this point, I don't know what else can I do ... Anyone knows how do I fix this??

In CORS browser always send simple cross domain request like GET requests to the server. But browser check the response header(Access-Control-Allow-Origin) from the server whether cross domain request is allowed by the server. That's why your GET request is hitting the server. But non simple requests like POST, PUT, DELETE browser can't let the requests to hit the server. Instead browser sends preflight request to the server to check whether it allows cross domain request. If the response from the server contains the headers for support cross domain requests, only then browser will perform the actual request. This pre-flight request is send as OPTIONS request. That's why your error saying response to preflight request doesn't pass. After we enable spring security all the preflight requests are blocked for non-authenticated users by spring security. To allow preflight request through we have to configure in the security configuration.

在此处输入图片说明

Remove disable() after the csrf().Then add the @CrossOrigin annotation at the top of the method. You can mention the domain inside the annotation as well. @CrossOrigin("http://localhost:3000")

This is the only thing you have to do.

A preflight request is a small request that is sent by the browser before the actual request. It contains information like which HTTP method is used, as well as if any custom HTTP headers are present (like Access-Control-Allow-Methods ).

When performing POST action, there are 2 steps. The first one is making an OPTIONS request, hence preflight request. Once the preflight is done, your browser knows what request types are allowed or denied.

In your case, the issue is that while your back-end expects 1 call, you are sending 2 (preflight + actual request) .

To solve this problem, you have to tell your back-end to respond OK when an OPTIONS request arrives and validate the others.

DISCLAIMER: I didn't try it with the way you configured cors ( CorsConfigurationSource ). But, let me show it with a simple filter bean

@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CorsFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        final HttpServletResponse response = (HttpServletResponse) res;
        response.setHeader("Access-Control-Allow-Origin", "*"); 
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        response.setHeader("Access-Control-Allow-Headers", ""Authorization, Cache-Control, Content-Type"");
        response.setHeader("Access-Control-Max-Age", "3600");
        
        //SEND OK or validate
        if ("OPTIONS".equalsIgnoreCase(((HttpServletRequest) req).getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(req, res);
        }
    }

    @Override
    public void destroy() {
        //
    }

    @Override
    public void init(FilterConfig config) throws ServletException {
        //
    }
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM