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

6 comments:

  1. Jagpreet Singh24/8/15 23:15

    A very good read! However I am unable to get it to work for POST.

    I don't understand why do I get "Allow: GET, OPTIONS" in response headers...

    Response Headers:

    HTTP/1.1 405 Request method 'POST' not supported
    X-Content-Type-Options: nosniff
    X-XSS-Protection: 1; mode=block
    Pragma: no-cache
    X-Frame-Options: DENY
    Set-Cookie: JSESSIONID=8p13w1ammseqinvw0z9uhy54;Path=/
    Allow: GET, OPTIONS
    Content-Type: text/html;charset=ISO-8859-1
    Cache-Control: must-revalidate,no-cache,no-store
    Content-Length: 1420
    Server: Jetty(8.1.10.v20130312)

    ReplyDelete
  2. Хорошо проделанная работа с отличным результатом!

    ReplyDelete
  3. Hi, I find reading this article a joy. It is extremely helpful and interesting and very much looking forward to reading more of your work..
    Brandable domains

    ReplyDelete
  4. next replica ysl bags australia our website zeal replica bags reviews like this replica bags london

    ReplyDelete
  5. บริษัท slot pg จะเล่นเกมไหน ก็สนุกสนานสุดสนุก และรับเงินรางวัล PGSLOT ในเกมมาก สล็อตเว็บไซต์ตรง เปิดให้บริการ พร้อมเกมสล็อต ที่จะทำให้ท่าน รับเงินรางวัลมั่นใจ เกมใหม่พลาด

    ReplyDelete