目录

CORS 跨域资源共享

Cross-origin resource sharing

跨域资源共享(英语:Cross-origin resource sharing,缩写:CORS),用于让网页的受限资源能够被其他域名的页面访问的一种机制。

通过该机制,页面能够自由地使用不同源(英语:cross-origin)的图片、样式、脚本、iframes以及视频。一些跨域的请求(特别是Ajax)常常会被同源策略(英语:Same-origin policy)所禁止。跨源资源共享定义了一种方式,为的是浏览器和服务器之间能互相确认是否足够安全以至于能使用跨源请求(英语:cross-origin requests)。比起纯粹的同源请求,这将更为自由和功能性的(functionality),但比纯粹的跨源请求更为安全。

跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同网域传来沙盒脚本的方法,以避开浏览器的同源策略。

什么是跨域

跨域请求存在的原因:由于浏览器的同源策略,即属于不同域的页面之间不能相互访问各自的页面内容。

  • 域名不同
  • 二级域名不同
  • 端口不同,协议不同

跨域测试

随便打开一个网站后打开 [开发者工具],里面的【Console】可以直接输入js代码测试:

1
2
3
4
5
6
7
8
9
var Authorization= "Bear zU6CUSVs88DjC7hkzpeZGpUpphs=";
var xhr = new XMLHttpRequest();
xhr.open('GET', 'http://localhost:8080/code');
xhr.setRequestHeader("Authorization",Authorization);
xhr.send(null);
xhr.onload = function(e) {
    var xhr = e.target;
    console.log(xhr.responseText);
}

解决跨域的方式

jsonp 安全性差,已经不推荐

CORS(W3C标准,跨域资源共享 - Cross-origin resource sharing) 服务端设置,安全性高,推荐使用

websocket 殊场景时使用,不属于常规跨域操作

代理服务(nginx)可作为服务端cors配置的一种方式,推荐使用

CORS 是什么

首先我们要明确,CORS 是什么,以及规范是如何要求的。这里只是梳理一下流程,具体的规范请看 这里。

CORS 全称是 Cross-Origin Resource Sharing,直译过来就是跨域资源共享。要理解这个概念就需要知道域、资源和同源策略这三个概念。

域,指的是一个站点,由 protocal、host 和 port 三部分组成,其中 host 可以是域名,也可以是 ip ;port 如果没有指明,则是使用 protocal 的默认端口 资源,是指一个 URL 对应的内容,可以是一张图片、一种字体、一段 HTML 代码、一份 JSON 数据等等任何形式的任何内容 同源策略,指的是为了防止 XSS,浏览器、客户端应该仅请求与当前页面来自同一个域的资源,请求其他域的资源需要通过验证。 了解了这三个概念,我们就能理解为什么有 CORS 规范了:从站点 A 请求站点 B 的资源的时候,由于浏览器的同源策略的影响,这样的跨域请求将被禁止发送;为了让跨域请求能够正常发送,我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送,这样的机制就是 CORS。

预检请求

在 CORS 中,定义了一种预检请求,即 preflight request,当实际请求不是一个 简单请求 时,会发起一次预检请求。预检请求是针对实际请求的 URL 发起一次 OPTIONS 请求,并带上下面三个 headers :

Origin:值为当前页面所在的域,用于告诉服务器当前请求的域。如果没有这个 header,服务器将不会进行 CORS 验证。 Access-Control-Request-Method:值为实际请求将会使用的方法 Access-Control-Request-Headers:值为实际请求将会使用的 header 集合 如果服务器端 CORS 验证失败,则会返回客户端错误,即 4xx 的状态码。

否则,将会请求成功,返回 200 的状态码,并带上下面这些 headers:

Access-Control-Allow-Origin:允许请求的域,多数情况下,就是预检请求中的 Origin 的值 Access-Control-Allow-Credentials:一个布尔值,表示服务器是否允许使用 cookies Access-Control-Expose-Headers:实际请求中可以出现在响应中的 headers 集合 Access-Control-Max-Age:预检请求返回的规则可以被缓存的最长时间,超过这个时间,需要再次发起预检请求 Access-Control-Allow-Methods:实际请求中可以使用到的方法集合 浏览器会根据预检请求的响应,来决定是否发起实际请求。

有时你会发现明明请求的是POST、GET、PUT、DELETE,但是浏览器中看到的确实OPTION,,为什么会变成OPTION?

原因:因为本次Ajax请求是“非简单请求”,所以请求前会发送一次预检请求(OPTIONS),这个操作由浏览器自己进行。如果服务器端后台接口没有允许OPTIONS请求,将会导致无法找到对应接口地址,因此需要服务端提供相应的信息到response header中,继续往下看。

后端需要返回的Header有哪些?

# 服务端允许访问的域名
Access-Control-Allow-Origin=https://idss-uat.jiuyescm.com
# 服务端允许访问Http Method
Access-Control-Allow-Methods=GET, POST, PUT, DELETE, PATCH, OPTIONS
# 服务端接受跨域带过来的Cookie,当为true时,origin必须是明确的域名不能使用*
Access-Control-Allow-Credentials=true
# Access-Control-Allow-Headers 表明它允许跨域请求包含content-type头,我们这里不设置,有需要的可以设置
#Access-Control-Allow-Headers=Content-Type,Accept
# 跨域请求中预检请求(Http Method为Option)的有效期,20天,单位秒
Access-Control-Max-Age=1728000

ps. 如果跨域需要携带cookie去请求,Access-Control-Allow-Credentials必须为true,但是需要注意当Access-Control-Allow-Credentials=true时,Access-Control-Allow-Origin就不能为” * “ ,必须是明确的域名,当然可以多个域名使用 “,” 分割

CORS 工作原理

跨域资源共享标准描述了,新的HTTP头部在浏览器有权限的时候,应该以如何的形式发送请求到远程URLs。虽然服务器会有一些校验和认证,但是浏览器有责任去支持这些头部以及增加相关的限制。

对于能够修改数据的Ajax和HTTP请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。

跨域资源共享(CORS)是 JSONP 模式的现代版。与 JSONP 不同,CORS 除了 GET 请求方法以外也支持其他的 HTTP 请求。用 CORS 可以让网页设计师用一般的 XMLHttpRequest,这种方式的错误处理比 JSONP 要来的好。另一方面,JSONP 可以在不支持 CORS 的老旧浏览器上运作。现代的浏览器都支持 CORS。

自定义 Filter/HandlerInterceptor

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自定义一个Filter来处理CORS跨域请求
@Component
public class CORSFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
  }

  // TODO:这里应该是只需要处理OPTIONS请求即可~~~
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    HttpServletResponse response = (HttpServletResponse) servletResponse;
    response.setHeader("Access-Control-Allow-Origin", "*");
    response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
    response.setHeader("Access-Control-Max-Age", "3600");
    response.setHeader("Access-Control-Allow-Headers", "content-type,Authorization");
    // response.setHeader("Access-Control-Allow-Credentials", "true");
    filterChain.doFilter(servletRequest, servletResponse);
  }

  @Override
  public void destroy() {
  }
}
  • Filter 由 Servlet 标准定义,要求 Filter 需要在 Servlet 被调用之前调用,作用顾名思义,就是用来过滤请求。在 Spring Web 应用中,DispatcherServlet 就是唯一的 Servlet 实现。
  • Interceptor 由 Spring 自己定义,由 DispatcherServlet 调用,可以定义在 Handler 调用前后的行为。这里的 Handler ,在多数情况下,就是我们的 Controller 中对应的方法。

Nginx 统一配置

 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
#
# Wide-open CORS config for nginx
#
location / {
     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #
        # Custom headers and headers various browsers *should* be OK with but aren't
        #
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        #
        # Tell client that this pre-flight info is valid for 20 days
        #
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
     }
     if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
     }
     if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' '*' always;
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS' always;
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range' always;
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range' always;
     }
}

Spring MVC 中的 CORS

在Spring4.2之前,官方没有提供内置的支持,所以那时都是自己使用Filter/拦截器来处理。它的唯一缺点就是可能没那么灵活和优雅,后续官方提供标注支持后能力更强更为灵活了(底层原理都一样)

DefaultCorsProcessor 它遵循的是W3C标准实现的。Spring MVC中对CORS规则的校验,都是通过委托给 DefaultCorsProcessor实现的

注入 CorsFilter

Spring4.2之前一般自己去实现一个这样的Filter来处理,4.2之后框架提供了内置支持。

Reactive 的叫 org.springframework.web.cors.reactive.CorsWebFilter

它的工作完全委托给CorsProcessor去处理的。此Filter可以与DelegatingFilterProxy一起使用,以帮助初始化且可以使用Spring容器内的Bean。注意CorsFilter在框架内部默认是木有配置的,若有需要请自行配置。

 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
public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
	...
	
	// 使用javax.servlet.ServletContainerInitializer方式注册Filter
    @Override
    protected void registerDispatcherServlet(ServletContext servletContext) {
        super.registerDispatcherServlet(servletContext);

        // 注册Jar包内 内置的Filter等等
        UrlBasedCorsConfigurationSource confiurationSource = new UrlBasedCorsConfigurationSource();
        // 根据URL配置其对应的CORS配置 key支持的是AntPathMatcher.match()
        // 说明:这里请使用LinkedHashMap,确保URL的匹配顺序(/**请放在最后一个)
        Map<String, CorsConfiguration> corsConfigs = new LinkedHashMap<>();
        //corsConfigs.put("*", new CorsConfiguration().applyPermitDefaultValues());
        // '/**'表示匹配所有深度的路径
        corsConfigs.put("/**", new CorsConfiguration().applyPermitDefaultValues());
        confiurationSource.setCorsConfigurations(corsConfigs);

        // /*表示所有请求都用此filter处理一下
        servletContext.addFilter("corsFilter", new CorsFilter(confiurationSource))
                .addMappingForUrlPatterns((EnumSet.of(DispatcherType.REQUEST)), false, "/*");
	}
}

@Configuration
public class CorsConfiguration {
@Bean
    public CorsFilter corsFilter() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        // 请求常用的三种配置,*代表允许所有,当时你也可以自定义属性(比如header只能带什么,只能是post方式等等)
        List<String> list = new ArrayList<>();
        list.add("http://localhost:80");
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        corsConfiguration.setAllowedOrigins(list);

        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfig());
        return new CorsFilter(source);
    }
}

注入 CorsFilter 不止这一种方式,还可以通过注入一个 FilterRegistrationBean 来实现

@CrossOrigin 注解

Spring MVC 提供了此注解来帮助你解决CORS跨域问题,比你使用Filter更加的方便,且能实现更加精细化的控制(一般可以和CorsFilter一起来使用,效果更佳)。 Spring Web MVC和Spring WebFlux在RequestMappingHandlerMapping里都是支持此注解的,该注解配置参数的原理可参考CorsConfiguration。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@CrossOrigin(origins = "*", allowedHeaders = "*")
@Controller
public class HomeController 
{
    @GetMapping(path="/")
    public String homeInit(Model model) {
        return "home";
    }
    @CrossOrigin(origins = "http://example.com")
    @GetMapping(path="/test")
    public String homeTest(Model model) {
        return "home/test";
    }
}

实现 WebMvcConfigurer.addCorsMappings 方法

WebMvcConfigurer 是一个接口,它同样来自于 Spring Web。我们可以通过实现它的 addCorsMappings 方法来针对全局 API 配置 CORS 规则:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Configuration
@EnableWebMvc
public class CorsConfiguration implements WebMvcConfigurer
{
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedMethods("GET", "POST")
                .allowedOrigins("http://localhost:63342")
                .maxAge(300L);;
    }
}

In spring boot application, it is recommended to just declare a WebMvcConfigurer bean.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Configuration
public class CorsConfiguration 
{
    @Bean
    public WebMvcConfigurer corsConfigurer() 
    {
        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**").allowedOrigins("http://localhost:8080");
            }
        };
    }
}

自定义Filter/HandlerInterceptor

前面有说到,Spring直到4.2版本才提供了对CORS的支持,因此对于一些老项目,一般会使用自定义的Filter/拦截器来处理:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自定义一个Filter来处理CORS跨域请求
@Component
public class CORSFilter implements Filter {
  @Override
  public void init(FilterConfig filterConfig) throws ServletException {
  }

  // TODO:这里应该是只需要处理OPTIONS请求即可~~~
  @Override
  public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
    	HttpServletResponse response = (HttpServletResponse) servletResponse;
    	response.setHeader("Access-Control-Allow-Origin", "*");
    	response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
    	response.setHeader("Access-Control-Max-Age", "3600");
    	response.setHeader("Access-Control-Allow-Headers", "content-type,Authorization");
    	// response.setHeader("Access-Control-Allow-Credentials", "true");
    	filterChain.doFilter(servletRequest, servletResponse);
  }

  @Override
  public void destroy() {
  }
}

Nginx统一配置

配置在Nginx后,后端服务就不用再操心跨域请求问题了,这是很多公司推荐的方案。 此处我贴出一个配置供以参考,copy自这里

 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
#
# Wide-open CORS config for nginx
#
location / {
	
	#### 对OPTIONS请求会设置很多的请求头并返回204
     if ($request_method = 'OPTIONS') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        #
        # Custom headers and headers various browsers *should* be OK with but aren't
        #
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        #
        # Tell client that this pre-flight info is valid for 20 days
        #
        add_header 'Access-Control-Max-Age' 1728000;
        add_header 'Content-Type' 'text/plain; charset=utf-8';
        add_header 'Content-Length' 0;
        return 204;
     }
     if ($request_method = 'POST') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
     }
     if ($request_method = 'GET') {
        add_header 'Access-Control-Allow-Origin' '*';
        add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
        add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range';
        add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range';
     }
}

面是自定义方式解决,不强依赖于Spring MVC框架的支持。那么下面就是使用Spring4.2后提供的能力来灵活解决,这当然也是生厂上主流使用的方案。

Spring Security

在引入了 Spring Security 之后,我们会发现前面的方法都不能正确的配置 CORS,每次 preflight request 都会得到一个 401 的状态码,表示请求没有被授权。这时,我们需要增加一点配置才能让 CORS 正常工作

To enable CORS support through Spring security, configure CorsConfigurationSource bean and use HttpSecurity.cors() configuration.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and()
            //other config
    }
 
 // 或者,干脆不实现 WebMvcConfigurer.addCorsMappings 方法或者注入 CorsFilter ,而是注入一个 CorsConfigurationSource ,同样能与上面的代码配合,正确的配置 CORS
    @Bean
    CorsConfigurationSource corsConfigurationSource() 
    {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("https://example.com"));
        configuration.setAllowedMethods(CorsConfiguration.ALL);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }
}

Spring MVC处理CORS请求的流程

Spring MVC处理任何一个reuqest请求都会去找到它的一个处理器Handler,因此首当其冲就来到DispatcherServlet#getHandler()这个方法~

getHandler()

对于Spring MVC来说,每处理一个request请求都应该对应着一个Handler:就是DispatcherServlet.getHandler()方法来找到其对应的处理器:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
DispatcherServlet
	// 根据HttpRequest从handlerMappings找到对应的handler
	@Nullable
	protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		if (this.handlerMappings != null) {

			// 开启Spring MVC后默认情况下handlerMappings的长度是4
			for (HandlerMapping mapping : this.handlerMappings) {
				HandlerExecutionChain handler = mapping.getHandler(request);
				if (handler != null) {
					return handler;
				}
			}
		}
		return null;
	}

handlerMappings它的长度默认是3,内容如下:

https://ask.qcloudimg.com/http-save/yehe-6158873/o8ylq3fsyz.png?imageView2/2/w/1620

处理本例请求的是RequestMappingHandlerMapping,获取处理器的方法在父类上:

 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
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
AbstractHandlerMapping
	
	// 默认使用的是UrlBasedCorsConfigurationSource来管理跨域配置
	private CorsConfigurationSource corsConfigurationSource = new UrlBasedCorsConfigurationSource();

	// 使用的都是本类的pathMatcher和urlPathHelper
	public void setCorsConfigurations(Map<String, CorsConfiguration> corsConfigurations) {
		Assert.notNull(corsConfigurations, "corsConfigurations must not be null");
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.setCorsConfigurations(corsConfigurations);
		source.setPathMatcher(this.pathMatcher);
		source.setUrlPathHelper(this.urlPathHelper);
		this.corsConfigurationSource = source;
	}
	// @since 5.1 此方法出现较晚,但一般也不建议去设置
	public void setCorsConfigurationSource(CorsConfigurationSource corsConfigurationSource) {
		Assert.notNull(corsConfigurationSource, "corsConfigurationSource must not be null");
		this.corsConfigurationSource = corsConfigurationSource;
	}


	@Override
	@Nullable
	public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
		// getHandlerInternal这个方法是根据URL去匹配一个Handler,当然有可能是匹配不上的,那么handler就为null
		Object handler = getHandlerInternal(request);
		if (handler == null) {
			handler = getDefaultHandler();
		}
		// 若最终还是为null,那就返回null 后续的也就不再处理了
		// 它的结果是:交给下一个HandlerMapping处理,若所有的处理完后还是返回null。
		// 那就noHandlerFound(processedRequest, response) --> 404
		if (handler == null) { 
			return null;
		}
		...
		HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
		...
	
		// 若是跨域请求,这里就继续处理,也是本文讲述具有差异性的地方所在
		if (CorsUtils.isCorsRequest(request)) {
			
			// 1、全局配置:从UrlBasedCorsConfigurationSource找到一个属于这个请求的配置
			// 请注意:若三种方式都没有配置,这里返回的就是null~~~
			CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
			
			// 2、从handler自己里找:若handler自己实现了CorsConfigurationSource接口,那就从自己这哪呗
			// 说明:此种方式适用于一个类就是一个处理器的case。比如servlet处理器
			// 所以对于@RequestMapping情况,这个值大部分情况都是null
			CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
			
			// 3、把全局配置和handler配置combine组合合并
			CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
		
			// 4、这个方法很重要。请看下面这个方法
			executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
		}
	}

	// @since 4.2
	protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request, HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
		
		// 若是预检请求:就new一个新的HandlerExecutionChain。
		// PreFlightHandler是一个HttpRequestHandler哦~~~并且实现了接口CorsConfigurationSource
		if (CorsUtils.isPreFlightRequest(request)) {
			HandlerInterceptor[] interceptors = chain.getInterceptors();
			chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
		}
			
		// 若不是预检请求,就添加一个拦截器CorsInterceptor
		// 注意:这个拦截器只会作用于这个chain哦(也就是这个handler~~~) 
		// 能进来这里是简单请求 或者 真实请求。
		else {
			chain.addInterceptor(new CorsInterceptor(config));
		}
		return chain;
	}

根据URL成功匹配到一个Handler后,若是跨域请求就会继续添加跨域部分的处理逻辑:

  • 若是预检请求:针对此请求会直接new一个PreFlightHandler作为HttpRequestHandler处理器来处理它,而不再是交给匹配上的Handler去处理(这点特别的重要) - PreFlightHandler#handle方法委托给了corsProcessor去处理跨域请求头、响应头的 - 值得注意的是:此时即使原Handler它不执行了,但匹配上的HandlerInterceptor们仍都还是会生效执行作用在OPTIONS方法上的
  • 若是简单请求/真实请求:在原来的处理链上加一个拦截器chain.addInterceptor(new CorsInterceptor(config)),由这个拦截器它最终复杂来处理相关逻辑(全权委托给corsProcessor

核心的处理步骤就这么简单,理解起来也并不困难。因此我们还非常有必要的就是这三种配置方式是如何被初始化的呢?

CorsFilter 方式初始化

要让它生效就需要我们手动把它注册进Servlet容器内,由它“拦截请求”自己来完成CorsProcessor.processRequest(corsConfiguration, request, response)这些处理操作。所以它和后续的getHandler()等这些处理逻辑是关系不大的。 此种方式的优雅程度上和自己实现差异并不大,因此我个人是不太推荐的~~

WebMvcConfigurer.addCorsMappings()方式初始化

这种方式是我推荐的,它的基本原理和我之前说过的WebMvcConfigurer其它配置项差不多。它作用的地方就是下面我列出的4个HandlerMapping初始化的时候。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
WebMvcConfigurationSupport
	@Bean
	public RequestMappingHandlerMapping requestMappingHandlerMapping() {
		...
	}
	// 最终返回的是个SimpleUrlHandlerMapping 可以直接完成映射
	@Bean
	@Nullable
	public HandlerMapping viewControllerHandlerMapping() {
		ViewControllerRegistry registry = new ViewControllerRegistry(this.applicationContext);
		... 
	}
	// 按照bean名称进行匹配处理器
	@Bean
	public BeanNameUrlHandlerMapping beanNameHandlerMapping() {}
	// 最终也是个SimpleUrlHandlerMapping 
	@Bean
	@Nullable
	public HandlerMapping resourceHandlerMapping() {}

他们四个初始化时最终都调用了同一个方法:mapping.setCorsConfigurations(getCorsConfigurations())设置CORS配置,此方法是父类AbstractHandlerMapping提供的,原理可参考CorsRegistry和CorsRegistration

@CrossOrigin初始化

关于此注解的初始化,在完成mapping注册的时候就已经完成了,大致步骤如下:

org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping#initCorsConfiguration

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
AbstractHandlerMethodMapping

	// 注册一个mapping
	public void registerMapping(T mapping, Object handler, Method method) {
		this.mappingRegistry.register(mapping, handler, method);
	}

	// 内部类
	class MappingRegistry {
		// 记录着没一个HandlerMethod所对应的注解配置
		private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();
		...
		public void register(T mapping, Object handler, Method method) {
			...
			// initCorsConfiguration这里就是解析handler上面的注解喽~~~
			// 此init方法只有RequestMappingHandlerMapping子类重写了~~~
			CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
			if (corsConfig != null) { // 若不为null(有注解配置),就缓存起来
				this.corsLookup.put(handlerMethod, corsConfig);
			}
			...
		}
	}

对于handler上次注解的解析,最终是由RequestMappingHandlerMapping完成的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
RequestMappingHandlerMappin
	@Override
	protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
		...
		// 找到类上和方法上的注解(若都为null就返回null)
		// 说明:此注解可以标注在父类、接口上
		CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
		CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);
		... // combine合并这两个部分(若有两个的话)
		
		// 最终执行它:兜底(防止注解上很多属性都木填)
		return config.applyPermitDefaultValues();
	}

它显著的特点是:和Handler强绑定,因此在注册Mapping的时候就完成初始化工作。

综上所述可得出这三种配置方式的区别:

  1. CorsFilter方式:完全独立的Filter,和其它配置并不冲突和也无关联,最终委托给CorsProcessor来完成的
  2. addCorsMappings方式:它的配置会作用于所有的内置配置的HandlerMapping上,所以它就是global全局配置
  3. @CrossOrigin方式:它和某一个具体的handler强绑定,所以它属于局部配置

说明:方式2和方式3可以形成互补配置,有combine的效果。



为何OPTIONS请求进入不了Controller的Handler方法内?

这个问题是系列文章的第一篇我抛出来的,因为有一个现象是:简单请求我可以在Controller的方法内向response手动添加请求头搞定。但是非简单请求这么做行不通了,原因是OPTIONS请求根本进入不了方法体~

阅读完本文的上半拉,此问题的答案就显而易见了,因此我此处不再废话。倘若对此问题还没想到答案的小伙伴,欢迎你在下面给我留言我会及时解答你的。

为何给response设置响应头写在postHandle()方法内无效?

这个问题倒是困扰了我好一会,直到我直到了Spring MVC对它的处理过程。 问题的现象是:response的响应头都有,但http状态码却是403,跨域失败。结果如下截图:

https://ask.qcloudimg.com/http-save/yehe-6158873/95l9bcgx73.png?imageView2/2/w/1620

针对此问题作出如下解释供以参考:

  1. 上面有说到一句话:匹配上handler后,若是OPTIONS请求的话,它最终的handler不是原handler而是一个全新的PreFlightHandler处理器,并且并且并且chain上的拦截器们都是会生效的
  2. 关键就在这里:PreFlightHandler执行handler处理方法最终是委托给CorsProcessor执行的,**config == null**并且是 预检请求 ,那它就会执行:**rejectRequest(serverResponse)**,这时状态码就已经设置为了403了,因此等handler方法执行完成之后再执行postHandle()方法体,因为返回状态码已经设置好,已经无力回天了,so就出现了如此怪异现象~

有人说在postHandle()方法里加上这么一句,手动把响应码改成200:response.setStatus(HttpStatus.OK.value());。 效果:能达到想要的跨域效(真实请求能继续发送)。但是我强烈不建议你这么去做,因此这样你需要加很多逻辑判断(什么时候应该设置,什么时候不应该),得不偿失。

DispatcherServlet.doOptions()方法简单分析

说明:dispatchOptionsRequest这个参数虽然默认值是false,但在DispatcherServlet所有的构造器里都有这么一句:setDispatchOptionsRequest(true)

 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
FrameworkServlet

	/** Should we dispatch an HTTP OPTIONS request to {@link #doService}?. */
	private boolean dispatchOptionsRequest = false;

	@Override
	protected void doOptions(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		
		// 若dispatchOptionsRequest = true 或者是预检请求OPTIONS请求,都会processRequest
		// processRequest(request, response);就是复杂的视图渲染逻辑~~~
		if (this.dispatchOptionsRequest || CorsUtils.isPreFlightRequest(request)) {
			processRequest(request, response);
			// 若你自己设置了allow响应头,那就不处理了。否则交给下面处理
			if (response.containsHeader("Allow")) {
				// Proper OPTIONS response coming from a handler - we're done.
				return;
			}
		}

		// Use response wrapper in order to always add PATCH to the allowed methods
		// 开发者自己没有设置Allow这个响应头就会进这里来,最终效果是
		// Allow:GET, HEAD, POST, PUT, DELETE, OPTIONS, PATCH
		super.doOptions(request, new HttpServletResponseWrapper(response) {
			@Override
			public void setHeader(String name, String value) {
				if ("Allow".equals(name)) {
					value = (StringUtils.hasLength(value) ? value + ", " : "") + HttpMethod.PATCH.name();
				}
				super.setHeader(name, value);
			}
		});
	}
若CORS请求的URL不存在,响应码404还是403?
  • 无默认的servlet处理器(DefaultServletHandler):404(找不到对应的handler)
  • 有默认的servlet处理器:403(能找到handler,因为有默认的处理器兜底嘛)

Spring MVC的这个配置用于开启默认处理器与否:

1
2
3
4
5
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        //configurer.enable();
        //configurer.enable("default");
    }

Spring Webflux

The @CrossOrigin annotation has the following default configuration:

  • Allows all origins (that explains the ‘*’ value in the response header)
  • Allows all headers
  • All HTTP methods mapped by the handler method are allowed
  • Credentials are not enabled
  • The ‘max-age’ value is of 1800 seconds (30 minutes)
 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
@CrossOrigin
@PutMapping("/cors-enabled-endpoint")
public Mono<String> corsEnabledEndpoint() {
    // ...
}

@Configuration
@EnableWebFlux
public class CorsGlobalConfiguration implements WebFluxConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry corsRegistry) {
        corsRegistry.addMapping("/**")
          .allowedOrigins("http://allowed-origin.com")
          .allowedMethods("PUT")
          .maxAge(3600);
    }
}
// 推荐
@Bean
CorsWebFilter corsWebFilter() {
    CorsConfiguration corsConfig = new CorsConfiguration();
    corsConfig.setAllowedOrigins(Arrays.asList("http://allowed-origin.com"));
    corsConfig.setMaxAge(8000L);
    corsConfig.addAllowedMethod("PUT");
    corsConfig.addAllowedHeader("Ynthm-Allowed");

    UrlBasedCorsConfigurationSource source =
      new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", corsConfig);

    return new CorsWebFilter(source);
}

Spring Cloud

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Bean
    public CorsWebFilter corsWebFilter() {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();

        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        // 版本不同 addAllowedOrigin("*"); 最新版本推荐
        corsConfiguration.addAllowedOriginPattern(CorsConfiguration.ALL);
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setMaxAge(3600L);

        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(source);
    }
1
2
3
4
5
cors:
  allowedOriginPatterns:
    - '*'
    - http://10.0.0.185:80
    - http://localhost:8888
 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
@Data
@Configuration
@ConfigurationProperties(prefix = "cors")
public class CorsProperties {
  private List<String> allowedOriginPatterns;
}

@Configuration
public class ResourcesConfig implements WebMvcConfigurer {

    @RefreshScope
    @Bean
    public CorsWebFilter corsWebFilter(CorsProperties corsProperties) {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration corsConfiguration = new CorsConfiguration();

        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);

        corsConfiguration.setAllowedOriginPatterns(corsProperties.getAllowedOriginPatterns());
        corsConfiguration.setAllowCredentials(true);
        corsConfiguration.setMaxAge(3600L);

        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsWebFilter(source);
    }
}

public class CorsWebFilter implements WebFilter {
  private final CorsConfigurationSource configSource;
}

nacos 修改会自动更新 ConfigurationProperties。@RefreshScope 修饰注入 bean 通过 /actuator/refresh 刷新无法生效。 CorsWebFilter 中的 configSource 是 final 修饰。需要自定义一个 CorsWebFilter 不启动应用刷新。

总结

  • 实现 WebMvcConfigurer.addCorsMappings 方法来进行的 CORS 配置,最后会在 Spring 的 Interceptor 或 Handler 中生效
  • 注入 CorsFilter 的方式会让 CORS 验证在 Filter 中生效
  • 引入 Spring Security 后,需要调用 HttpSecurity.cors 方法以保证 CorsFilter 会在身份验证相关的 Filter 之前执行
  • HttpSecurity.cors + WebMvcConfigurer.addCorsMappings 是一种相对低效的方式,会导致跨域请求分别在 Filter 和 Interceptor 层各经历一次 CORS 验证
  • HttpSecurity.cors + 注册 CorsFilter 与 HttpSecurity.cors + 注册 CorsConfigurationSource 在运行的时候是等效的
  • 在 Spring 中,没有通过 CORS 验证的请求会得到状态码为 403 的响应

附录