CORS explained in detail

CORS is a W3C standard, the full name is Cross-origin resource sharing.

It allows the browser to cross-origin server, issued XMLHttpRequest/fetch request, thus overcoming the AJAX can only be used in the same source of the limitations.

1. Introduction

CORS requires both browser and server support. Currently, all browsers support this feature, and Internet Explorer cannot be lower than IE10.

The entire CORS communication process is done automatically by the browser, without user involvement. For developers, CORS communication with the same source of AJAX communication no difference, the code is exactly the same. Once the browser finds the AJAX request cross-source, it will automatically add some additional headers, and sometimes one more additional request, but the user will not feel it.

Therefore, the key to achieve CORS communication is the server. As long as the server implements the CORS specification, it can communicate across sources.

2. Two types of requests

Browsers divide CORS requests into two categories: simple requests and not-so-simple requests.

As long as the following two conditions are met, it is a simple request.

  1. The request method is one of the following three methods.

    • HEAD
    • GET
    • POST
  2. The HTTP headers do not exceed the following fields.

    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Type: limited to three values application/x-www-form-urlencoded, multipart/form-data, text/plain

This is to be compatible with forms, because historically forms have been able to send cross-domain requests. AJAX’s cross-domain design is that, as long as the form can send, AJAX can send directly.

Where the above two conditions are not met at the same time, it is not a simple request.

The browser processing of these two requests, is not the same.

3. Simple request

3.1 Basic flow

For a simple request, the browser makes a CORS request directly. Specifically, it adds an Origin field to the header.

Here is an example, the browser finds that the cross-origin AJAX request is a simple request, it automatically adds an Origin field to the header information.

1
2
3
4
5
6
GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

In the above header, the Origin field is used to indicate the source (protocol + domain + port) from which this request is coming. Based on this value, the server decides whether to grant the request or not.

If the source specified by Origin is not within the scope of the permission, the server returns a normal HTTP response. The browser finds that this response does not contain the Access-Control-Allow-Origin field in its headers (see below for details) and knows that there is an error, thus throwing an error that is caught by the onerror callback function of XMLHttpRequest. Note that such errors cannot be identified by status codes, as the status code of an HTTP response can be 200.

If the domain name specified by Origin is within the permitted range, the server returns a response with a few extra header fields.

1
2
3
4
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

The above header contains three fields related to CORS requests, all starting with Access-Control-.

  • Access-Control-Allow-Origin

    This field is required. Its value is either the value of the Origin field at the time of the request, or a * that indicates that a request for an arbitrary domain name is accepted.

  • Access-Control-Allow-Credentials

    This field is optional. Its value is a boolean indicating whether cookies are allowed to be sent, and by default, cookies are not included in CORS requests. A value of true means that the server has explicitly given permission for the cookie to be included in the request and sent to the server together. This value can also only be set to true, so if the server does not want the browser to send cookies, just delete the field.

  • Access-Control-Expose-Headers

    This field is optional. CORS request, the XMLHttpRequest object getResponseHeader() method can only get six basic fields: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma. If you want to get other fields, you must specify them in the Access-Control-Expose-Headers. In the above example, getResponseHeader('FooBar') will successfully get the value of the FooBar field.

    The wildcard character: *, proposed in the latest specification, indicates that the client is allowed to access all header. is not yet implemented in some browsers.

3.2 withCredentials property

As mentioned above, CORS requests do not send cookies and HTTP authentication information by default. If a cookie is to be sent to the server, on the one hand, the server has to agree to respond to the Access-Control-Allow-Credentials field.

1
Access-Control-Allow-Credentials: true

On the other hand, the developer must turn on the withCredentials property in the AJAX request

1
2
3
4
5
6
// XMLHttpRequest
var xhr = new XMLHttpRequest();
xhr.withCredentials = true;

// fetch
fetch('/foo', {credentials: 'include'})

Otherwise, even if the server agrees to send a cookie, the browser will not send it. Or, if the server asks to set a cookie, the browser will not handle it.

However, some browsers will still send cookies together if the withCredentials setting is omitted, in which case withCredentials can be turned off explicitly.

1
2
3
4
5
// XMLHttpRequest
xhr.withCredentials = false;

// fetch
fetch('/foo', {credentials: 'omit'})

Note that to send a cookie, Access-Control-Allow-Origin cannot be set to an asterisk, but must specify a domain name that is explicit and consistent with the requested page. At the same time, cookies still follow the same origin policy, only cookies set with the server domain will be uploaded, cookies from other domains will not be uploaded, and the document.cookie in the (cross-origin) original web code will not be able to read cookies from the server domain.

4. Non-simple requests

4.1 Preflight Requests

A non-simple request is one that has special requirements for the server, such as a request method of PUT or DELETE, or a Content-Type field of type application/json.

A CORS request that is not a simple request adds an HTTP query request, called a preflight, before the formal communication.

The browser first asks the server if the domain name of the current web page is in the server’s permission list, and which HTTP verbs and header fields can be used. Only when it gets a positive answer does the browser issue a formal XMLHttpRequest request, otherwise it throws an exception.

Here is a JavaScript script for the browser.

1
2
3
4
5
var url = 'http://api.alice.com/cors';
var xhr = new XMLHttpRequest();
xhr.open('PUT', url, true);
xhr.setRequestHeader('X-Custom-Header', 'value');
xhr.send();

In the above code, the HTTP request method is PUT and sends a custom header X-Custom-Header.

The browser finds that this is a non-simple request and automatically issues a “preflight” request to ask the server to confirm that it can do so. Here is the HTTP header for this “preflight” request.

1
2
3
4
5
6
7
8
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

The request method used for the “preflight” request is OPTIONS, which indicates that the request is for interrogation. Inside the header, the key field is Origin, which indicates the source of the request.

In addition to the Origin field, the header of a “preflight” request includes two special fields.

  • Access-Control-Request-Method

    This field is required to list which HTTP methods will be used by the browser’s CORS request, in the above example PUT.

  • Access-Control-Request-Headers

    This field is a comma-separated string that specifies the additional header field that will be sent by the browser CORS request, in the above example X-Custom-Header.

4.2 Response to Preflight Requests

After the server receives the Preflight request, it checks the Origin, Access-Control-Request-Method and Access-Control-Request-Headers fields and confirms that cross-origin requests are allowed, then it can respond.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

The key thing in the HTTP response above is the Access-Control-Allow-Origin field, which indicates that http://api.bob.com is allowed to initiate the request. This field can also be set to an asterisk to indicate consent to any cross-origin request.

1
Access-Control-Allow-Origin: *

If the server denies the preflight request, a normal HTTP response is returned, but without any CORS-related header fields. At this point, the browser determines that the server did not agree to the preflight request and therefore triggers an error that is caught by the onerror callback function of the XMLHttpRequest object. The console will print the following error message.

1
2
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

The other CORS-related fields that the server responds to are listed below.

1
2
3
4
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
Access-Control-Max-Age: 1728000
  • Access-Control-Allow-Methods

    This field is required and its value is a comma-separated string indicating all methods supported by the server for cross-domain requests. Note that all supported methods are returned, not just the one requested by the browser. This is to avoid multiple “preflight” requests.

  • Access-Control-Allow-Headers

    The Access-Control-Allow-Headers field is required if the browser request includes the Access-Control-Request-Headers field. It is also a comma-separated string indicating all header fields supported by the server, not limited to the fields requested by the browser in the Preflight.

  • Access-Control-Allow-Credentials

    This field has the same meaning as in the case of a simple request.

  • Access-Control-Max-Age

    This field is optional and is used to specify the validity of this preflight request in seconds. In the above result, the validity period is 20 days (1728000 seconds), which means that the response is allowed to be cached for 1728000 seconds (i.e. 20 days), during which time another preflight request does not have to be issued.

4.3 Normal browser request and response

Once the server has passed the “preflight” request, each subsequent normal browser CORS request will have an Origin header field, just like a simple request. The server’s response will also have an Access-Control-Allow-Origin header field.

Here is the normal CORS request from the browser after the “preflight” request.

1
2
3
4
5
6
7
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

The Origin field in the above header is automatically added by the browser.

Here is the normal response from the server.

1
2
Access-Control-Allow-Origin: http://api.bob.com
Content-Type: text/html; charset=utf-8

In the above header, the Access-Control-Allow-Origin field is always included in every response.

CORS is used for the same purpose as JSONP, but is more powerful than JSONP. JSONP only supports GET requests, while CORS supports all types of HTTP requests. the advantages of JSONP are support for older browsers and the ability to request data from sites that do not support CORS.

Spring With CORS

spring provides a number of ways to implement CORS.

@CrossOrigin

CORS can be easily implemented using the @CrossOrigin annotation. The code is as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/hello")
//@CrossOrigin  // It can also be annotated on the class.
public class HelloController {

    @GetMapping(produces = "text/plain; charset=utf-8")
    @CrossOrigin(
        // Access-Control-Allow-Origin
        origins = { "*" },
        
        // Alternative to origins that supports more flexible originpatterns. 
        // Please, see CorsConfiguration.setAllowedOriginPatterns(List)for details.
        // originPatterns = { "" },   
        
        // Access-Control-Allow-Credentials
        allowCredentials = "false",
        
        // Access-Control-Allow-Headers
        allowedHeaders = { "*" },
        
        // Access-Control-Expose-Headers
        exposedHeaders = { "*" },
        
        // Access-Control-Max-Age
        maxAge = 60 * 30,
        
        // Access-Control-Allow-Methods
        methods = {RequestMethod.GET, RequestMethod.DELETE, RequestMethod.POST, RequestMethod.PUT}
    )
    public String hello() {
        return "hello";
    }
}

Note that When allowedCredentials is true, allowedOrigins cannot contain the special value * because it cannot be set in the Access-Control-Allow-Origin response header. To allow credentials from a set of sources, list them explicitly or consider using allowedOriginPatterns instead.

Otherwise, an exception will be thrown when the application starts.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
Caused by: java.lang.IllegalArgumentException: When allowCredentials is true, allowedOrigins cannot contain the special value "*" since that cannot be set on the "Access-Control-Allow-Origin" response header. To allow credentials to a set of origins, list them explicitly or consider using "allowedOriginPatterns" instead.
    at org.springframework.web.cors.CorsConfiguration.validateAllowCredentials(CorsConfiguration.java:473) ~[spring-web-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.register(AbstractHandlerMethodMapping.java:650) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.registerHandlerMethod(AbstractHandlerMethodMapping.java:332) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.registerHandlerMethod(RequestMappingHandlerMapping.java:420) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.registerHandlerMethod(RequestMappingHandlerMapping.java:76) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lambda$detectHandlerMethods$2(AbstractHandlerMethodMapping.java:299) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at java.base/java.util.LinkedHashMap.forEach(LinkedHashMap.java:721) ~[na:na]
    at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.detectHandlerMethods(AbstractHandlerMethodMapping.java:297) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.processCandidateBean(AbstractHandlerMethodMapping.java:266) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.initHandlerMethods(AbstractHandlerMethodMapping.java:225) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.afterPropertiesSet(AbstractHandlerMethodMapping.java:213) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.afterPropertiesSet(RequestMappingHandlerMapping.java:205) ~[spring-webmvc-5.3.18.jar:5.3.18]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1863) ~[spring-beans-5.3.18.jar:5.3.18]
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1800) ~[spring-beans-5.3.18.jar:5.3.18]
    ... 16 common frames omitted

WebMvcConfiguration

Global CORS configuration can be done through the addCorsMappings method of the WebMvcConfiguration configuration interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package io.springboot.demo.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfiguration implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        
        registry
            // Enable cross-origin request handling for the specified path pattern. 
            // Exact path mapping URIs (such as "/admin") are supported as well as Ant-style path patterns (such as "/admin/**"). 
            .addMapping("/*")
            .allowedOrigins("*")
            // .allowedOriginPatterns("")
            .allowCredentials(false)
            .allowedHeaders("*")
            .exposedHeaders("*")
            .maxAge(60 *30)
            .allowedMethods("*")
            ;
    }
}

Similarly, if allowCredentials is true, allowedOrigins cannot be *.

CorsFilter

CorsFilter is specifically designed to handle CORS requests. This approach is the most flexible and allows you to write code to handle CORS requests.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
import java.util.List;
import java.util.stream.Stream;

import javax.servlet.Filter;

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpHeaders;
import org.springframework.util.StringUtils;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.filter.CorsFilter;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Configuration
public class CorsFilterConfiguration {

    @Bean
    public FilterRegistrationBean<Filter> corsFilter() {
        FilterRegistrationBean<Filter> filterRegistrationBean = new FilterRegistrationBean<Filter>();
        filterRegistrationBean.setFilter(new CorsFilter(request -> {
            String origin = request.getHeader(HttpHeaders.ORIGIN);

            if (!StringUtils.hasText(origin)) {
                // Not a CORS request.
                return null;
            }

            /**
            * TODO You can write your own logic code to determine whether to allow the
            * current request or not.
            */

            log.info("origin = {}", origin);

            CorsConfiguration configuration = new CorsConfiguration();

            configuration.addAllowedOrigin(origin);

            String accessControlRequestHeaders = request.getHeader(HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS);
            if (StringUtils.hasText(accessControlRequestHeaders)) {
                Stream.of(accessControlRequestHeaders.split(",")).map(String::trim).distinct()
                        .forEach(configuration::addAllowedHeader);
            }

            configuration.addExposedHeader("*");

            configuration.setAllowCredentials(true);

            configuration
                    .setAllowedMethods(List.of("GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "TRACE"));

            return configuration;
        }));
        filterRegistrationBean.addUrlPatterns("/*");
        filterRegistrationBean.setOrder(Integer.MIN_VALUE); // Ensure first execution
        return filterRegistrationBean;
    }
}

Reference