CORS with Spring MVC

In this blog post I will explain how to implement Cross-Origin Resource Sharing (CORS) on a Spring MVC backend.

CORS is a W3C spec that allows cross-domain communication from the browser. Whenever a request is made from http://www.domaina.com to http://www.domainb.com, or even from http://localhost:8000 to http://localhost:9000, you will need to implement CORS on your backend.

To allow CORS we need to add the following headers to all Spring MVC responses:


Access-Control-Allow-Credentials: true

Access-Control-Allow-Origin: http://localhost:9000

Access-Control-Allow-Methods: GET, OPTIONS, POST, PUT, DELETE

Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept

Access-Control-Max-Age: 3600

The easiest way to do this is by creating an interceptor:


public class CorsInterceptor extends HandlerInterceptorAdapter {

 public static final String CREDENTIALS_NAME = "Access-Control-Allow-Credentials";
 public static final String ORIGIN_NAME = "Access-Control-Allow-Origin";
 public static final String METHODS_NAME = "Access-Control-Allow-Methods";
 public static final String HEADERS_NAME = "Access-Control-Allow-Headers";
 public static final String MAX_AGE_NAME = "Access-Control-Max-Age";

 @Override
 public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  response.setHeader(CREDENTIALS_NAME, "true");
  response.setHeader(ORIGIN_NAME, "http://localhost:9000");
  response.setHeader(METHODS_NAME, "GET, OPTIONS, POST, PUT, DELETE");
  response.setHeader(HEADERS_NAME, "Origin, X-Requested-With, Content-Type, Accept");
  response.setHeader(MAX_AGE_NAME, "3600");
  return true;
 }

}

Then we register this interceptor in our web configuration:

public class WebMvcConfig extends WebMvcConfigurerAdapter {
  
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new CorsInterceptor());
    }

    ...

}

Now all GET requests will be handled correctly.

Modification requests

Whenever we do a modification request (POST, PUT, DELETE), our browser will first send a 'preflight' OPTIONS request. This is an extra security check to see if you can modify data. Because Spring MVC ignores OPTIONS requests by default, we will not get a CORS compliant response. We can overwrite this configuration as follows:

When using a Java configuration, in the DispatcherServletInitializer:


@Override
protected void customizeRegistration(Dynamic registration) {
    registration.setInitParameter("dispatchOptionsRequest", "true");
    super.customizeRegistration(registration);
}

Or in the web.xml:


<servlet>
    <servlet-name>yourServlet</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
     <param-name>dispatchOptionsRequest</param-name>
     <param-value>true</param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
</servlet>

Now we can write a simple handler for OPTIONS requests:

@Controller
public class OptionsController {

    @RequestMapping(method = RequestMethod.OPTIONS)
    public ResponseEntity handle() {
        return new ResponseEntity(HttpStatus.NO_CONTENT);
    }

}

This controller handles all OPTIONS requests, sending back a NO_CONTENT response with the desired CORS headers due to our interceptor. Now that OPTIONS respond correctly, the PUT, POST and DELETE will also work correctly.

Congratulations, you now have a CORS compliant Spring MVC backend :)

Multiple origins

Sometimes you have a backend service that is used by multiple applications and thus serves multiple origins. With some minor code changes we can implement this feature:


public class CorsInterceptor extends HandlerInterceptorAdapter {
    
    private static final Logger LOGGER = LoggerFactory.getLogger(CorsInterceptor.class);

    public static final String REQUEST_ORIGIN_NAME = "Origin";

    public static final String CREDENTIALS_NAME = "Access-Control-Allow-Credentials";
    public static final String ORIGIN_NAME = "Access-Control-Allow-Origin";
    public static final String METHODS_NAME = "Access-Control-Allow-Methods";
    public static final String HEADERS_NAME = "Access-Control-Allow-Headers";
    public static final String MAX_AGE_NAME = "Access-Control-Max-Age";

    private final List<String> origins;
    
    public CorsInterceptor(String origins) {
        this.origins = Arrays.asList(origins.trim().split("( )*,( )*"));
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.setHeader(CREDENTIALS_NAME, "true");
        response.setHeader(METHODS_NAME, "GET, OPTIONS, POST, PUT, DELETE");
        response.setHeader(HEADERS_NAME, "Origin, X-Requested-With, Content-Type, Accept");
        response.setHeader(MAX_AGE_NAME, "3600");
        
        String origin = request.getHeader(REQUEST_ORIGIN_NAME);
        if (origins.contains(origin)) {
            response.setHeader(ORIGIN_NAME, origin);
            return true; // Proceed
        } else {
            LOGGER.warn("Attempted access from non-allowed origin: {}", origin);
            // Include an origin to provide a clear browser error
            response.setHeader(ORIGIN_NAME, origins.iterator().next());
            return false; // No need to find handler
        }
    }

}

All we do now is checking if our request origin is in the list of allowed origins and echo it back into the response. Thus if somebody makes a request from 'domain-a.com' we return back that same 'domain-a.com' as allowed origin, while for 'domain-b.com' we return 'domain-b.com'.

Because the list of allowed origins is provided as string, we can simply define our origins in a properties file:

cors.origins=http://www.domain-a.com,http://www.domain-b.com