CORS & Spring CORS

"Spring CORS相关记录"

Posted by tablesheep on June 18, 2021

“CORS & Spring CORS相关记录”

基础概念

(仅仅只是小总结,具体可以看References)

同源政策规定浏览器请求只能发送给同源的网址。(同源指的是协议、域名、端口相同)

CORS是跨源资源分享(Cross-Origin Resource Sharing)的缩写,是为了跨域请求能够在同源政策下正常发送的一种机制。CORS需要浏览器(主流浏览器都支持)与服务器同时支持。

CORS将请求分为简单请求非简单请求,对于两种请求采取的策略不同。

简单请求

简单请求定义

  1. GETPOSTHEAD
  2. 允许人为设置的的请求头只包含对 CORS 安全的首部字段集合AcceptAccept-LanguageContent-LanguageContent-Typeapplication/x-www-form-urlencodedmultipart/form-datatext/plain))

对于简单请求只需要在请求头添加一个Origin表示此次请求的源,服务器会根据Origin、请求方法等判断是否允许跨域请求。

非简单请求

对于非简单请求,每次正常请求前会发送一个预检请求(PreFlight Request请求),此次请求为OPTIONS,会包含几个请求头给服务器判断是否允许。

1
2
3
Origin: https://www.baidu.com     //表示请求来源
Access-Control-Request-Method: GET  //请求所使用方法
Access-Control-Request-Headers: Header-1, Header-2 //请求将携带的Headers

如果预检请求不通过,服务器会返回一个没有CORS相关Headers的响应,此时浏览器就会报跨域错误。

如果请求通过则返回信息会有CORS相关Headers

1
2
3
4
5
Access-Control-Allow-Origin: https://www.baidu.com  //服务器允许的源,*表示所有源都可以跨域
Access-Control-Allow-Methods: POST, GET, OPTIONS //支持的所有跨域请求的方法
Access-Control-Allow-Headers: Header-1, Header-2  //支持的所有跨域请求头
Access-Control-Max-Age: 86400  //不一定有,表明该响应的有效时间(秒)。在有效时间内,浏览器无须为同一请求再次发起预检请求
Access-Control-Expose-Headers: Header-3, Header-4 //不一定有,默认情况下只有几种Header会暴露,可以通过它配置需要暴露的Header

Credentials请求

另外对于携带Credentials(凭证,比如Cookie)的请求也有不同的处理,当请求配置携带Credentials时,服务器必须返回如下响应头。

1
Access-Control-Allow-Credentials: true  //另外,当允许附带Credentials时,Access-Control-Allow-Origin不能用*

VaryCORS

Vary是一个响应头部,而它的值是请求头,主要跟请求的缓存有关系。

当缓存服务器收到一个请求,只有当前的请求和原始(缓存)的请求头跟缓存的响应头里的Vary都匹配,才能使用缓存的响应。—-带Vary头的响应

1
2
3
4
Vary: Origin
Vary: Access-Control-Request-Method
Vary: Access-Control-Request-Headers
如果服务端指定了具体的域名而非“*”,那么响应首部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。(不过Spring目前是都会返回)

Spring CORS 配置

CorsConfiguration

CORS配置类,几乎所有的跨域配置都与之有关

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
public class CorsConfiguration {
   ......
   @Nullable
   private List<String> allowedOrigins;  //后端允许的源,对应Access-Control-Allow-Origin

   @Nullable
   private List<OriginPattern> allowedOriginPatterns; //也可使用Pattern动态匹配源

   @Nullable
   private List<String> allowedMethods;  //后端允许的请求方法

   @Nullable
   private List<HttpMethod> resolvedMethods = DEFAULT_METHODS; //后端允许的请求方法

   @Nullable
   private List<String> allowedHeaders;  //后端允许的请求头

   @Nullable
   private List<String> exposedHeaders; //对应Access-Control-Expose-Headers

   @Nullable
   private Boolean allowCredentials; //是否允许携带凭证,对应Access-Control-Allow-Credentials

   @Nullable
   private Long maxAge; //预检请求有效时间,对应Access-Control-Max-Age

   ......
}

@CrossOrigin

通过@CrossOrigin可以最细粒度的配置方法或者Controller。

@CrossOrigin大致工作原理:

1.Handler注册时,解析@CrossOrigin配置并且缓存起来

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

private final Map<HandlerMethod, CorsConfiguration> corsLookup = new ConcurrentHashMap<>();

......

public void register(T mapping, Object handler, Method method) {
   ......
//根据initCorsConfiguration模板方法获取CORS配置
      CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
      if (corsConfig != null) {
         corsConfig.validateAllowCredentials();
         //校验通过后将CORS配置缓存在Map中
         this.corsLookup.put(handlerMethod, corsConfig); 
      }

      this.registry.put(mapping,
            new MappingRegistration<>(mapping, handlerMethod, directPaths, name, corsConfig != null));
    ......
}

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
//获取@CrossOrigin,最终生成CorsConfiguration
@Override
protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
   HandlerMethod handlerMethod = createHandlerMethod(handler, method);
   Class<?> beanType = handlerMethod.getBeanType();
   CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
   CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);

   if (typeAnnotation == null && methodAnnotation == null) {
      return null;
   }

   CorsConfiguration config = new CorsConfiguration();
   updateCorsConfig(config, typeAnnotation);
   updateCorsConfig(config, methodAnnotation);

   if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
      for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
         config.addAllowedMethod(allowedMethod.name());
      }
   }
   return config.applyPermitDefaultValues();
}

2.在获取Hander时会获取CORS配置,然后根据配置在HandlerExecutionChain添加CorsInterceptor进行跨域校验

获取CORS配置调用链

根据CorsConfiguration创建CorsInterceptor逻辑 AbstractHandlerMapping#getHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
......
//判断是否有CORS配置或者是否预检请求
   if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
   //获取CORS配置
      CorsConfiguration config = getCorsConfiguration(handler, request);
      if (getCorsConfigurationSource() != null) {
         CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
         config = (globalConfig != null ? globalConfig.combine(config) : config);
      }
      if (config != null) {
         config.validateAllowCredentials();
      }
      //添加拦截器
      executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
   }

   return executionChain;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
		HandlerExecutionChain chain, @Nullable CorsConfiguration config) {

	if (CorsUtils.isPreFlightRequest(request)) {
		HandlerInterceptor[] interceptors = chain.getInterceptors();
		return new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
	}
	else {
	//最终会根据CorsConfiguration创建CorsInterceptor
		chain.addInterceptor(0, new CorsInterceptor(config));
		return chain;
	}
}

WebMvcConfigurer#addCorsMappings

可以通过实现WebMvcConfigurer进行CORS全局配置

1
2
3
4
5
6
7
8
9
10
@Configuration
public class CustomizeWebConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins(CorsConfiguration.ALL)
                .allowedMethods(CorsConfiguration.ALL)
                .allowedHeaders(CorsConfiguration.ALL);
    }
}

具体原理是在WebMvcConfigurationSupport注册MVC组件时(几个AbstractHandlerMapping),会根据WebMvcConfigurationSupport#getCorsConfigurations获取到我们的配置并且合并成CorsConfigurationSource,最终也是在AbstractHandlerMapping#getHandler通过CorsConfigurationSource获取CorsConfiguration并且添加CorsInterceptor进行跨域校验

具体AbstractHandlerMapping#getHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
......
   if (hasCorsConfigurationSource(handler) || CorsUtils.isPreFlightRequest(request)) {
      CorsConfiguration config = getCorsConfiguration(handler, request);
      if (getCorsConfigurationSource() != null) {
      //获取CorsConfigurationSource,根据CorsConfigurationSource获取全局CorsConfiguration
         CorsConfiguration globalConfig = getCorsConfigurationSource().getCorsConfiguration(request);
         //将全局配置与细粒度配置结合
         config = (globalConfig != null ? globalConfig.combine(config) : config);
      }
      if (config != null) {
         config.validateAllowCredentials();
      }
      executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
   }

   return executionChain;
}

CorsFilter

可以通过CorsFilter通过过滤器进行配置(WebFlux是CorsWebFilter

1
2
3
4
5
6
7
8
9
10
@Bean
public CorsFilter corsFilter() {
    UrlBasedCorsConfigurationSource source=  new UrlBasedCorsConfigurationSource();
    CorsConfiguration corsConfiguration = new CorsConfiguration();
    corsConfiguration.addAllowedOrigin(CorsConfiguration.ALL);
    corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
    corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
    source.registerCorsConfiguration("/**", corsConfiguration);
    return new CorsFilter(source);
}

小结

@CrossOriginWebMvcConfigurer最终是通过CorsInterceptor实现,而CorsFilter是过滤器,区别就是FilterInterceptor 的区别,FilterDispatcherServlet 调用前,InterceptorDispatcherServlet 之后,Hander(Controller)之前。

问题记录

Gateway重复配置

问题:在微服务改造过程中可能会存在网关与应用服务都存在CORS配置情况,此时浏览器会因为重复的CORS header而出现不能正确处理的情况。(The ‘Access-Control-Allow-Origin’ header contains multiple values ‘xxx, xxx’, but only one is allowed)

解决:Spring Cloud Gateway 提供了DedupeResponseHeaderGatewayFilterFactory以解决Response Header重复问题。

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
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
/*
Use case: Both your legacy backend and your API gateway add CORS header values. So, your consumer ends up with
          Access-Control-Allow-Credentials: true, true
          Access-Control-Allow-Origin: https://musk.mars, https://musk.mars
(The one from the gateway will be the first of the two.) To fix, add
          DedupeResponseHeader=Access-Control-Allow-Credentials Access-Control-Allow-Origin
Configuration parameters:
- name
    String representing response header names, space separated. Required.
- strategy
	RETAIN_FIRST - Default. Retain the first value only.
	RETAIN_LAST - Retain the last value only.
	RETAIN_UNIQUE - Retain all unique values in the order of their first encounter.
Example 1
      default-filters:
      - DedupeResponseHeader=Access-Control-Allow-Credentials
Response header Access-Control-Allow-Credentials: true, false
Modified response header Access-Control-Allow-Credentials: true
Example 2
      default-filters:
      - DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_LAST
Response header Access-Control-Allow-Credentials: true, false
Modified response header Access-Control-Allow-Credentials: false
Example 3
      default-filters:
      - DedupeResponseHeader=Access-Control-Allow-Credentials, RETAIN_UNIQUE
Response header Access-Control-Allow-Credentials: true, true
Modified response header Access-Control-Allow-Credentials: true
 */

/**
 * @author Vitaliy Pavlyuk
 */
public class DedupeResponseHeaderGatewayFilterFactory
		extends AbstractGatewayFilterFactory<DedupeResponseHeaderGatewayFilterFactory.Config> {

	private static final String STRATEGY_KEY = "strategy";

	public DedupeResponseHeaderGatewayFilterFactory() {
		super(Config.class);
	}

	@Override
	public List<String> shortcutFieldOrder() {
		return Arrays.asList(NAME_KEY, STRATEGY_KEY);
	}

	@Override
	public GatewayFilter apply(Config config) {
		return new GatewayFilter() {
			@Override
			public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
				return chain.filter(exchange)
						.then(Mono.fromRunnable(() -> dedupe(exchange.getResponse().getHeaders(), config)));
			}

			@Override
			public String toString() {
				return filterToStringCreator(DedupeResponseHeaderGatewayFilterFactory.this)
						.append(config.getName(), config.getStrategy()).toString();
			}
		};
	}

	public enum Strategy {

		/**
		 * Default: Retain the first value only.
		 */
		RETAIN_FIRST,

		/**
		 * Retain the last value only.
		 */
		RETAIN_LAST,

		/**
		 * Retain all unique values in the order of their first encounter.
		 */
		RETAIN_UNIQUE

	}

	void dedupe(HttpHeaders headers, Config config) {
		String names = config.getName();
		Strategy strategy = config.getStrategy();
		if (headers == null || names == null || strategy == null) {
			return;
		}
		for (String name : names.split(" ")) {
			dedupe(headers, name.trim(), strategy);
		}
	}

	private void dedupe(HttpHeaders headers, String name, Strategy strategy) {
		List<String> values = headers.get(name);
		if (values == null || values.size() <= 1) {
			return;
		}
		switch (strategy) {
		case RETAIN_FIRST:
			headers.set(name, values.get(0));
			break;
		case RETAIN_LAST:
			headers.set(name, values.get(values.size() - 1));
			break;
		case RETAIN_UNIQUE:
			headers.put(name, new ArrayList<>(new LinkedHashSet<>(values)));
			break;
		default:
			break;
		}
	}

	public static class Config extends AbstractGatewayFilterFactory.NameConfig {

		private Strategy strategy = Strategy.RETAIN_FIRST;

		public Strategy getStrategy() {
			return strategy;
		}

		public Config setStrategy(Strategy strategy) {
			this.strategy = strategy;
			return this;
		}
	}
}

如下:

1
2
3
4
5
spring:
  cloud:
    gateway:
      default-filters:
       - DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST

References