Cross domain resource sharing of CORS with Spring Boot

Posted by pornophobic on Tue, 25 Jan 2022 01:19:46 +0100

CORS is a W3C standard, whose full name is called "cross origin resource sharing"; Before introducing CORS in detail, first briefly introduce what is the homology policy, so as to understand the origin and necessity of CORS.

Browser homology policy

"Homology policy" is the cornerstone of browser security. At present, all browsers implement this policy.

meaning

The so-called "homology" refers to the following three same:

  • Same agreement
  • Same domain name
  • Same port

for instance:

Current web addressRequested page addressCross domain (different sources)reason
http://www.wu-yikun.top/page.htmlhttp://www.wu-yikun.top/main.htmlnoSame protocol (http), same domain name (www.wu-yikun.top), same port (80)
http://www.wu-yikun.top/page.htmlhttps://www.wu-yikun.top/other.htmlyesDifferent protocols (http and https)
http://www.wu-yikun.top/page.htmlhttp://www.wu-yikun.com/page.htmlyesDifferent domain names (www.wu-yikun.top and www.wu-yikun.com)
http://www.wu-yikun.top/page.htmlhttp://www.wu-yikun.top:8090/other.htmlyesDifferent ports (80 and 8090)

Now let's look at this picture. Is it clear at a glance?

objective

Excerpt from teacher Ruan Yifeng's article

The purpose of homology policy is to ensure the security of user information and prevent malicious websites from stealing data.

Imagine this situation: website A is A bank. After users log in, they browse other websites. What happens if other websites can read the cookies of website A?

Obviously, if the Cookie contains privacy (such as total deposits), this information will be leaked. What's more terrible is that cookies are often used to save the login status of users. If users don't log out, other websites can impersonate users and do whatever they want. Because the browser also stipulates that submitting forms is not limited by the homology policy.

It can be seen that the "homology policy" is necessary, otherwise cookies can be shared, and the Internet will have no security at all.

limit

With the development of the Internet, the homology policy is becoming more and more strict. If it is not homologous, the following behaviors will be limited.

  • Cookie s, LocalStorage and IndexDB cannot be read
  • DOM cannot get
  • AJAX request could not be sent

Just because an AJAX request cannot be sent can curb many reasonable uses.

Next, it introduces how to realize cross domain resource sharing to avoid homology policy.

CORS details

brief introduction

CORS requires both browser and server support. At present, all browsers support this function. In the process of CORS communication, the browser will complete automatically without user participation. Therefore, in order to realize CORS communication, the key is to make the server support CORS interface and cross source communication.

CORS two types of requests

The browser divides CORS requests into two categories: simple requests and non simple requests.

Simple request

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

  • The request method is one of three types
    • HEAD
    • GET
    • POST
  • The HTTP header information does not exceed the following fields
    • Accept
    • Accept-Language
    • Content-Language
    • Content type: only limited to one of application/x-www-form-urlencoded, multipart / form data and text/plain

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

Here is a simple CORS request:

The following is the request message sent by the browser to the server:

GET /resources/public-data/ HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example

The request header field Origin indicates that the request originated from http://foo.example , take another look at the response message:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2021 00:23:53 GMT
Server: Apache/2
Access-Control-Allow-Origin: *
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Transfer-Encoding: chunked
Content-Type: application/xml

[XML Data]

In this example, the access control allow origin returned by the server: * indicates that the resource can be accessed by any foreign domain.

Access-Control-Allow-Origin: *

use Origin and Access-Control-Allow-Origin Can complete the simplest access control. If the server only allows from https://foo.example The contents of the header field are as follows:

Access-Control-Allow-Origin: https://foo.example

Non simple request

Non simple requests are those that have special requirements for the server. For example, the request method is PUT or DELETE, or the content type field type is application/json.

A major feature of non simple CORS requests is that an HTTP query request will be added before formal communication, which is called "Pre check" request (Preflight request). The method of the "pre check" request is OPTIONS. The use of the "pre check" request can avoid the unexpected impact of cross domain requests on the server's user data.

The following is an HTTP request that needs to perform a pre check request:

As described below, the actual POST request does not carry the access control request - * header. They are only applicable to OPTIONS requests.

The following is the complete information interaction between the server and the client.

The first is the pre inspection request:

OPTIONS /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
Origin: https://foo.example
Access-Control-Request-Method: POST
Access-Control-Request-Headers: X-PINGOTHER, Content-Type

The browser has detected that requests originating from JavaScript need to be pre checked. From the above message, we can see that lines 1 to 10 send a "pre inspection request" using the OPTIONS method. OPTIONS is a method defined in HTTP/1.1 protocol to obtain more information from the server. This method has no impact on server resources. The pre inspection request carries the following two header fields at the same time:

  • Access-Control-Request-Method: POST
  • Access-Control-Request-Headers: X-PINGOTHER, Content-Type

Header field Access-Control-Request-Method Tell the server that the actual request will use the POST method.

Header field Access-Control-Request-Headers Inform the server that the actual request will carry two custom request header fields: X-PINGOTHER and content type.

Based on this, the server determines whether the actual request is allowed.

Pre inspection response:

HTTP/1.1 204 No Content
Date: Mon, 01 Dec 2021 01:15:39 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: X-PINGOTHER, Content-Type
Access-Control-Max-Age: 86400
Vary: Accept-Encoding, Origin
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive

(1) The server's response carries access control allow origin: https://foo.example To limit the source domain of the request.

(2) At the same time, the access control allow methods carried indicates that the server allows the client to initiate requests using POST and GET methods (compared with Allow The response header is similar, but it has strict access control).

(3) The header field access control allow headers indicates that the server allows the fields X-PINGOTHER and content type to be carried in the request. If the browser request includes an access control request headers field, the access control allow headers field is required. It is also a comma separated string, indicating all header information fields supported by the server, not limited to the fields requested by the browser in "pre check".

(4) Finally, the header field access control Max age indicates that the effective time of the response is 86400 seconds, that is, 24 hours. Within the effective time, the browser does not need to send a pre check request again for the same request. Note that the browser itself maintains a Maximum effective time , if the value of the header field exceeds the maximum effective time, it will not take effect.

After the pre inspection request is completed, send the actual request:

POST /doc HTTP/1.1
Host: bar.other
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:71.0) Gecko/20100101 Firefox/71.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-us,en;q=0.5
Accept-Encoding: gzip,deflate
Connection: keep-alive
X-PINGOTHER: pingpong
Content-Type: text/xml; charset=UTF-8
Referer: https://foo.example/examples/preflightInvocation.html
Content-Length: 55
Origin: https://foo.example
Pragma: no-cache
Cache-Control: no-cache

Actual response message:

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2021 01:15:40 GMT
Server: Apache/2
Access-Control-Allow-Origin: https://foo.example
Vary: Accept-Encoding, Origin
Content-Encoding: gzip
Content-Length: 235
Keep-Alive: timeout=2, max=99
Connection: Keep-Alive
Content-Type: text/plain

In the header information above, the access control allow origin field must be included in each response.

Spring Boot solves cross domain problems

After talking so much, let's feel the actual scene.

Do you feel a certain "intimacy"? Let's solve this sense of powerlessness.

Manually set the response header through the Filter

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
@Slf4j
@WebFilter(urlPatterns = {"/*"}, filterName = "corsFilter")
public class CorsFilter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        log.info("Start cross domain filter");
    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse resp, FilterChain chain) throws IOException, ServletException {
        HttpServletResponse response = (HttpServletResponse) resp;
        // Manually set response headers to resolve cross domain access
        response.setHeader("Access-Control-Allow-Origin", "*");
        response.setHeader("Access-Control-Allow-Methods", "POST, PUT, GET, OPTIONS, DELETE");
        // Set expiration time
        response.setHeader("Access-Control-Max-Age", "86400");
        response.setHeader("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept, Authorization, uuid");
        // Support HTTP 1.1
        response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
        // Support HTTP 1.0 response. setHeader("Expires", "0");
        response.setHeader("Pragma", "no-cache");
        // code
        response.setCharacterEncoding("UTF-8");
        chain.doFilter(request, resp);
    }

    @Override
    public void destroy() {
        log.info("Destroy cross domain filters");
    }
}

Use @ CrossOrigin annotation (local cross domain)

@CrossOrigin annotation source code:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CrossOrigin {
    
    @AliasFor("origins")
    String[] value() default {};

    @AliasFor("value")
    String[] origins() default {};

    String[] allowedHeaders() default {};

    String[] exposedHeaders() default {};

    RequestMethod[] methods() default {};

    String allowCredentials() default "";

    long maxAge() default -1L;
}

Use @ CrossOrigin annotation:

@CrossOrigin(origins = "*", allowedHeaders = "*", maxAge = 86400)
@PostMapping("/login")
public String login(@RequestBody User user) {
	TODO..
}

However, the source code annotated by @ CrossOrigin determines that it can only be cross domain configured for a single interface, that is, local cross domain. Although it is easier to use, such as the Filter filter on, it is obviously not what we want, and this annotation is rarely used in actual development.

Implement WebMvcConfigurer interface

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.addMapping("/**")
            	// Indicates which domains are allowed to access. The simple point can be*
                .allowedOrigins("http://localhost:3000")
                .allowedHeaders("*")
                .allowedMethods("*")
            	// allowCredentials(true): indicates that identity credentials are attached
            	// Once the allowCredentials(true) method is used, allowedOrigins("*") needs to specify a specific domain, not*
                .allowCredentials(true)
                .maxAge(86400);
    }
}

The above method works normally when there is no Interceptor defined, but if you have a global Interceptor, such as the Interceptor that detects user login:

public class AuthenticationInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
        // Fetch the token from the http request header
        String token = httpServletRequest.getHeader("token");
        // Check whether you are logged in
        if (token == null) {
        	throw new InvalidTokenException(ResultCode.INVALID_TOKEN.getCode(), "Login information has expired, please login again");
        }
        return true;
    }
    
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {

    }
}

When the custom interceptor returns true, everything is normal, but when the interceptor throws an exception (or returns false), the subsequent CORS configuration will not take effect.

Why does the interceptor throw an exception and CORS doesn't take effect? Take a look at the issue proposed on GitHub:

when interceptor preHandler throw exception, the cors is broken #9595

The general contents are as follows:

Someone submitted an issue stating that if an exception is thrown in the preHandler method of the custom interceptor, the global CORS configuration set through CorsRegistry will fail, but the members of Spring Boot do not think it is a BUG ๐Ÿ›.

Then the author gives a specific example:

He first defined CorsRegistry and added a custom interceptor, which threw an exception:

Then it is found that AbstractHandlerMapping adds the interceptor of Cors to the end of the interceptor chain when adding CorsInterceptor:

That will cause the problem mentioned above: after throwing an exception in the custom interceptor, the CORS interceptor interceptor has no chance to set the CORS related response header in the response.

The issuer also gives a solution, that is, the interceptor corsinterpector used to process CORS is clamped at the first position of the interceptor chain:

In this way, once the request comes, the first interceptor will set the corresponding CORS response header for the response (e.g. access control allow origin, access control allow methods, access control allow headers). If other custom interceptors throw exceptions later, it will not have any impact!

I think this is a feasible solution, but members of Spring Boot believe that this is not a Bug of Spring Boot, but a Bug of Spring Framework, and all have closed this issue.

Inject CorsFilter filter

Using filters can avoid the conflict between interceptors and global cross domain configuration. The code is as follows:

@Configuration
public class CorsFilterConfiguration {

    @Bean
    public CorsFilter corsFilter() {
        // Add configuration after creating the CorsConfiguration object
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // Set which original fields to release
        corsConfiguration.addAllowedOrigin("*");
        // Which original request header information is released
        corsConfiguration.addAllowedHeader("*");
        // Which request methods are released
        corsConfiguration.addAllowedMethod("*");

        // Add mapping path
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);

        return new CorsFilter(source);
    }

}

โญ Why can filters avoid conflicts and interceptors don't?

Because the filter depends on the Servlet container, it can filter almost all requests based on function callbacks. The interceptor relies on Web frameworks (such as Spring MVC framework) and is implemented through AOP based on reflection.

The trigger sequence is as follows:

Because the filter is triggered before the interceptor, but if there are multiple filters, you also need to set CorsFilter as the first filter.

Other perspectives | ideas for solving cross domain CORS

The server supports CORS

๐Ÿคจ What are you expecting?

The [Spring Boot solves cross domain problems] (#Spring Boot solves cross domain problems) just mentioned is to explain how to realize CORS cross domain resource sharing on the server.

The above four methods are effective! If in doubt, see in the comments area.

๐Ÿ˜‰ If you use Spring framework instead of Spring Boot, I also found an official document for your reference: CORS support in Spring Framework

JSONP

Taking advantage of the vulnerability that the < script > tag does not have cross domain restrictions, web pages get JSON data dynamically generated from other sources across domains. JSONP requests must be supported by the server of the other party.

Note: three tags themselves allow resources to be loaded across domains.

  • <img src="xxx">
  • <link href="xxx">
  • <script src="xxx">

JSONP is the same as Ajax, which is the way that the client sends a request to the server and obtains data from the server. However, AJAX belongs to the same source policy and JSONP belongs to the non same source policy (supporting cross domain requests).

Advantages: it is simple and has good compatibility. It can be used to solve the problem of cross domain data access of mainstream browsers.

Disadvantages: only GET method is supported, which has limitations, is unsafe, and may be attacked by XSS.

Reverse proxy server

The same origin policy is the standard that the browser needs to follow. If the server requests from the server, it does not need to follow the same origin policy. Therefore, the cross domain problem can be effectively solved through the reverse proxy server. The proxy server needs to do the following:

  1. Accept client requests
  2. Forward the request to the actual server
  3. Return the response result of the server to the client

Nginx is a similar reverse proxy server. It solves cross domain problems by configuring nginx proxy across domains.

Reference

Topics: Java Spring Boot Back-end