Spring boot solves CORS (cross-domain) requests (based on spring MVC 4.0 and above).

I. Introduction

   Last night, before I got off work, my company's front-end colleagues suddenly told me that the method he sent from the front-end changed to OPTIONS. I saw that his code also said POST request, but why did it become OPTIONS? request, and the returned http status code is also 200, but no data is returned, which means that the request was intercepted and returned without entering the method at all. Where exactly does this go wrong?

2. Reason Exploration

    After tirelessly searching for information in the morning, I found the reason in the following blog post: https://www.cnblogs.com/cc299/p/7339583.html.

I briefly explain why:

The main reason for the cross-domain (CORS) problem is: if there is any set of different protocols, domain names, and ports between the page that sends the Ajax request on the front end and the server that processes the Ajax request on the back end, cross-domain problems will occur. See the table below ( source ) for details :

 

        Let's go back to the situation where I encountered this problem. Since our project is a project with a front-end and back-end separation, the front-end page and the back-end service are most likely not on the same host during the development stage and when they are officially launched, that is, there will be cross-domain connections. question.

        At the same time, browsers divide CORS requests into two categories: simple requests and not-so-simple requests.

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

(1) The request method is one of the following three methods:
HEAD
GET
POST
(2) In addition to the header information added by the browser itself, the HTTP header information does 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

Anything that does not satisfy the above two conditions is a non-simple request.

    Regarding the browser's processing methods for simple requests and non-simple requests, I will only mention one point: CORS requests for non-simple requests will add an HTTP query request before the formal communication, which is called a "preflight" request. The browser first asks the server whether the domain name of the current page is on the server's allow list, and which HTTP verbs and header fields can be used. The browser will issue a formal XMLHttpRequest request only if it gets a positive answer, otherwise it will report an error.

For details, please refer to: Ruan Yifeng's "Cross Domain Resource Sharing CORS Detailed Explanation"

Below is the ajax code where I simulate the frontend to send the request.

   $("#test").click(function(){
  htmlobj=$.ajax({
   method:"POST",
   headers: {
        Accept: "application/json; charset=utf-8",
        authorization:"123456"
    },
  url:"http://localhost:8769/appBuyerSaleScontract/addSaleScontractWithCodeByShopping",
  data:{"aaa":"bbb"}
  	});
  });
The result obtained is shown in the following figure:

In the ajax code, I define the request method to be POST, but here it becomes OPTIONS. This is because we are sending a non-simple request, so the browser sends a "preflight" before sending the real ajax request ” request, and the request method of the preflight request is OPTIONS. The result of this preflight request is 403, that is, the backend does not accept this request. The returned result also shows that this is an illegal cross-domain request.

3. Solutions

    Now that we know the cause, we can start working on solving the problem. Since the return value is 403, it means that it did enter our program, but it was intercepted and rejected by spring, so it did not enter the business code.

    We know that springMVC uses different handlers to process requests according to different request methods, and the OPTIONS request we send will be automatically processed by the processRequest method of the corsProcessor object (type: DefaultCorsProcessor).

 Note: If the return value in the code block in the figure below is true, it means that the request is allowed to access. If it is a "preflight" request, the result of 200 will be returned directly, and the browser will judge whether it can be executed (specifically, the judgment return Whether the Access-Control-Allow-XXX in the response header contains the value of the corresponding Access-Control-Request-XXX in the request header ) if it is false, it means that the request is rejected.


    Next, we enter the processRequest method to see.


The four points marked above are the key to this method and the key to whether our "preflight" request can be successfully executed.

1.

if (!CorsUtils.isCorsRequest(request)) {
    return true;
}

The purpose of this step is to determine whether the request is a cross-domain request, and the logic of the isCorsRequest(request) method is also very simple.

public static boolean isCorsRequest(HttpServletRequest request) {
    return request.getHeader("Origin") != null;
}

Just determine whether there is a request header Origin.

Normal cross-domain requests can return true. Pass by! A conversion becomes false.

If you enter and execute return true; it means that this is not a cross-domain request, and spring will directly return the http status code of 200. The same result is obtained when executing return true. in the following three steps.

The logic to be executed next is the logic in else.

2.

 
 
if (this.responseHasCors(serverResponse)) {
    logger.debug("Skip CORS processing: response already contains \"Access-Control-Allow-Origin\" header");
    return true;
}

The purpose of this step is to determine whether the response header "Access-Control-Allow-Origin" has been included in the response. If the header information has been included, return true directly; and execute return true;

Its execution code is as follows:

private boolean responseHasCors(ServerHttpResponse response) {
    try {
        return response.getHeaders().getAccessControlAllowOrigin() != null;
    } catch (NullPointerException var3) {
        return false;
    }
}

3.

if (WebUtils.isSameOrigin(serverRequest)) {
    logger.debug("Skip CORS processing: request is from same origin");
    return true;
}

The main purpose of this step is to determine whether the address of the socket (IP+port) that sends the request is the same as the address of the socket (IP+port) that receives the request (the protocol is not compared here).

Returns true if it is the same. execute return true;

Otherwise return false. Go to step 4.

The main logic is as follows:

public static boolean isSameOrigin(HttpRequest request) {
    String origin = request.getHeaders().getOrigin();
    if (origin == null) {
        return true;
    } else {
        UriComponentsBuilder urlBuilder;
        if (request instanceof ServletServerHttpRequest) {
            HttpServletRequest servletRequest = ((ServletServerHttpRequest)request).getServletRequest();
            urlBuilder = (new UriComponentsBuilder()).scheme(servletRequest.getScheme()).host(servletRequest.getServerName()).port(servletRequest.getServerPort()).adaptFromForwardedHeaders(request.getHeaders());
        } else {
            urlBuilder = UriComponentsBuilder.fromHttpRequest(request);
        }

        UriComponents actualUrl = urlBuilder.build();
        UriComponents originUrl = UriComponentsBuilder.fromOriginHeader(origin).build();
        return actualUrl.getHost().equals(originUrl.getHost()) && getPort(actualUrl) == getPort(originUrl);
    }
}

4.

The fourth step is the most crucial step. The above three steps determine whether there is a request header Origin, whether there is a response header "Access-Control-Allow-Origin" in the response, and whether the socket address of the request is the same as the socket address of the request source. As long as you have done interception and processing, you can basically enter step 4.

The main logic in step 4 is to determine whether there is a config (type: CorsConfiguration)

if (config == null)

Let's take a look at what the CorsConfiguration class has.

public class CorsConfiguration {
    public static final String ALL = "*";
    private static final List<HttpMethod> DEFAULT_METHODS;
    private List<String> allowedOrigins;
    private List<String> allowedMethods;
    private List<HttpMethod> resolvedMethods;
    private List<String> allowedHeaders;
    private List<String> exposedHeaders;
    private Boolean allowCredentials;
    private Long maxAge;
 

以上是这个类中的所有字段,我们主要关心的字段有如下几个:

   // CorsConfiguration
    public static final String ALL = "*";
    // 允许的请求源
    private List<String> allowedOrigins;
    // 允许的http方法
    private List<String> allowedMethods;    ——-->我们可以加入的http方法,我们注册时加入的允许方法。
    // 允许的http方法
    private List<HttpMethod> resolvedMethods;   ----->我们不能加入的http方法,后面比对的时候只能比对这个。
    // 允许的请求头
    private List<String> allowedHeaders;
    // 返回的响应头
    private List<String> exposedHeaders;
    // 是否允许携带cookies
    private Boolean allowCredentials;
    // 预请求的存活有效期
    private Long maxAge;

说了这么久终于说道重点上了。


在Spring MVC4 中为我们提供了这么一个配置类来完成跨域请求。

在springboot中只需要加上下面这个类就可以完成允许跨域请求。其中allowedHeaders(String ... headers)这个方法中设置的允许带上的请求头可以各位看官老爷们自己定义。

@Configuration
public class CORSConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry
                .addMapping("/**")
                .allowedMethods("*")
                .allowedOrigins("*")
                .allowedHeaders("authorization","Accept");
    }
}

There is one point to make here. allowedMethods and resolvedMethods are actually the same.

When we call allowedMethods(String ... method) , we actually call the CorsRegistration.setAllowedMethods (List<String> allowedMethods) method

Here is the source code for this method:

public void setAllowedMethods(List<String> allowedMethods) {
        this.allowedMethods = allowedMethods != null ? new ArrayList(allowedMethods) : null;
        if (!CollectionUtils.isEmpty(allowedMethods)) {
            this.resolvedMethods = new ArrayList(allowedMethods.size());
            Iterator var2 = allowedMethods.iterator();

            while(var2.hasNext()) {
                String method = (String)var2.next();
                if ("*".equals(method)) {
                    this.resolvedMethods = null;
                    break;
                }

                this.resolvedMethods.add(HttpMethod.resolve(method));//Convert the "GET", "POST" we entered into HttpMethod objects and add them to resolvedMethods
            }
        } else {
            this.resolvedMethods = DEFAULT_METHODS;
        }

    }

I have annotated the conversion logic of allowedMethods and resolvedMethods.

Next, we continue to look at the execution logic, which is the code in the else block.

return this.handleInternal(serverRequest, serverResponse, config, preFlightRequest);
The handleInternal method executes the main logic to determine whether cross-domain requests are allowed.

The following is the main logic of the method, which I will focus on.

 protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response, CorsConfiguration config, boolean preFlightRequest) throws IOException {
        String requestOrigin = request.getHeaders().getOrigin(); //获取请求源地址
        String allowOrigin = this.checkOrigin(config, requestOrigin); //检查是否有与requestOrigin一致的,我们设置的请求源,如果没有则返回null;
                                                                      //此处还有这样一段逻辑:如果你设置的allowOrigin为*,并且你设置的allowCredentials(是否允许带cookies)为false,则返回的allowOrigin会是*
                                                                      //                     如果你设置的allowOrigin为*,并且你设置的allowCredentials(是否允许带cookies)为true,则返回的allowOrigin会是requestOrigin
                                                                      //                     如果你设置的allowOrigin为真正的url,则会遍历整个List判断每个元素忽略大小写时是否相等。如相等则返回requestOrigin,否则返回null
                                                                      //                     如果你没有设置allowOrigin或者requestOrigin为空则直接返回null;
        HttpMethod requestMethod = this.getMethodToUse(request, preFlightRequest);//获取真正ajax发送时的请求方法
        List<HttpMethod> allowMethods = this.checkMethods(config, requestMethod); //检查是否有允许的请求方法,如果resolveMethod为null,则返回requestMethod,否则进行对比,如果没有,则返回null
        List<String> requestHeaders = this.getHeadersToUse(request, preFlightRequest);//获取真正ajax发送时的需要携带的请求头
        List<String> allowHeaders = this.checkHeaders(config, requestHeaders);  //检查是否有允许的请求头信息
                                                                                //此处还有这样一段逻辑:如果你设置的allowHeaders为*,则直接返回requestHeaders
                                                                                //                      否则逐个比对,知道全部匹配为止
        if (allowOrigin == null || allowMethods == null || preFlightRequest && allowHeaders == null) { //preFlightRequest:只要符合请求头包含Origin,请求方法为OPTIONS和请求头包含"Access-Control-Request-Method"即为true,我们发送的"预检"请求都包含这三样
            this.rejectRequest(response);
            return false;
        } else {
            HttpHeaders responseHeaders = response.getHeaders();
            responseHeaders.setAccessControlAllowOrigin(allowOrigin);
            responseHeaders.add("Vary", "Origin");
            if (preFlightRequest) {
                responseHeaders.setAccessControlAllowMethods(allowMethods); //设置响应头的Access-Control-Allow-Methods
            }

            if (preFlightRequest && !allowHeaders.isEmpty()) {  
                responseHeaders.setAccessControlAllowHeaders(allowHeaders);//设置响应头的Access-Control-Allow-Headers
            }

            if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
                responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());//设置响应头的Access-Control-Expose-Headers
            }

            if (Boolean.TRUE.equals(config.getAllowCredentials())) {
                responseHeaders.setAccessControlAllowCredentials(true);//设置响应头的Access-Control-Allow-Credentials
            }

            if (preFlightRequest && config.getMaxAge() != null) {
                responseHeaders.setAccessControlMaxAge(config.getMaxAge());//设置响应头的maxAge
            }

            response.flush();
            return true;
        }
    }
protected void rejectRequest(ServerHttpResponse response) throws IOException {
    response.setStatusCode(HttpStatus.FORBIDDEN);
    response.getBody().write("Invalid CORS request".getBytes(UTF8_CHARSET));
}

其中rejectRequest方法执行的逻辑就是为什么我们会得到403 forbidden!的结果。


三、疑问与测试

1.filter实现跨域问题的解决

我看了好多网上说的方法,其中有一种方法也非常的热门,就是用filter拦截

于是我写了下面这个类

@Component
public class CORSFilter implements Filter{
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("Init");
    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse resp, FilterChain filterChain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse)resp;
        HttpServletRequest request = (HttpServletRequest) req;
        System.out.println(request.getHeader("Access-Control-Request-Method"));
        if( request.getHeader("Access-Control-Request-Method") != null && request.getHeader("Access-Control-Request-Headers") != null ){
            response.setHeader("Access-Control-Allow-Origin","http://localhost:8080");
            response.setHeader("Access-Control-Allow-Methods",request.getHeader("Access-Control-Request-Method"));
            response.setHeader("Access-Control-Allow-Headers",request.getHeader("Access-Control-Request-Headers"));
            response.setHeader("Access-Control-Max-Age","300");
            response.setHeader("Access-Control-Allow-Credentials","true");
        }
        filterChain.doFilter(request,response);
    }
    @Override
    public void destroy() {
        System.out.println("destroy");
    }
}

下面是访问的结果。


同样的,使用filter也能够成功。同时也能够自定义设置值。但是这种方式不便于后期维护,同时看起来也比较乱,相比之下,我推荐上面的使用CorsRegistry 注册的方式。



Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=326442284&siteId=291194637