版本
Spring Cloud :Hoxton.SR12
Spring Cloud Gateway :
3.1.0
問題描述
在 SpringCloud 項目中,前後端分離目前很常見,在調試時,會遇到兩種情況的跨域:
前端頁面通過不同域名或IP通路微服務的背景
例如前端人員會在本地起HttpServer 直連背景開發本地起的服務,此時,如果不加任何配置,前端頁面的請求會被浏覽器跨域限制攔截,是以,業務服務常常會添加如下代碼設定全局跨域:
@Bean
public CorsFilter corsFilter() {
logger.debug("CORS限制打開");
CorsConfiguration config = new CorsConfiguration();
# 僅在開發環境設定為*
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("*");
config.setAllowCredentials(true);
UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
configSource.registerCorsConfiguration("/**", config);
return new CorsFilter(configSource);
}
前端頁面通過不同域名或IP通路SpringCloud Gateway
例如前端人員在本地起HttpServer直連伺服器的Gateway進行調試。此時,同樣會遇到跨域。需要在Gateway的配置檔案中增加:
spring:
cloud:
gateway:
globalcors:
cors-configurations:
# 僅在開發環境設定為*
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
那麼,此時直連微服務和網關的跨域問題都解決了,是不是很完美?
No~ 問題來了,****前端仍然會報錯:“不允許有多個’Access-Control-Allow-Origin’ CORS頭”。
Access to XMLHttpRequest at 'http://192.168.2.137:8088/api/two' from origin 'http://localhost:3200' has been blocked by CORS policy:
The 'Access-Control-Allow-Origin' header contains multiple values '*, http://localhost:3200', but only one is allowed.
仔細檢視傳回的響應頭,裡面包含了兩份Access-Control-Allow-Origin頭。我們用用戶端版的PostMan做一個模拟,在請求裡設定頭:Origin : * ,發現問題了:Access-Control-Allow-Origin 兩個頭重複了兩次,其中浏覽器對後者有唯一性限制!
解決的方案有兩種:
利用DedupeResponseHeader配置
spring:
cloud:
gateway:
globalcors:
cors-configurations:
'[/**]':
allowedOrigins: "*"
allowedHeaders: "*"
allowedMethods: "*"
default-filters: #解決cors重複跨域問題
- DedupeResponseHeader=Vary Access-Control-Allow-Origin Access-Control-Allow-Credentials, RETAIN_FIRST
DedupeResponseHeader加上以後會啟用DedupeResponseHeaderGatewayFilterFactory 在其中,dedupe方法可以按照給定政策處理值。
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, values.stream().distinct().collect(Collectors.toList()));
break;
default:
break;
}
}
如果請求中設定的Origin的值與我們自己設定的是同一個,例如生産環境設定的都是自己的域名xxx.com或者開發測試環境設定的都是*(浏覽器中是無法設定Origin的值,設定了也不起作用,浏覽器預設是目前通路位址),那麼可以選用RETAIN_UNIQUE政策,去重後傳回到前端。
如果請求中設定的Oringin的值與我們自己設定的不是同一個,RETAIN_UNIQUE政策就無法生效,比如 ”*“ 和 ”xxx.com“是兩個不一樣的Origin,最終還是會傳回兩個Access-Control-Allow-Origin 的頭。此時,看代碼裡,response的header裡,先加入的是我們自己配置的Access-Control-Allow-Origin的值,是以,我們可以将政策設定為RETAIN_FIRST ,隻保留我們自己設定的。
大多數情況下,我們想要傳回的是我們自己設定的規則,是以直接使用RETAIN_FIRST 即可。實際上,DedupeResponseHeader 可以針對所有頭,做重複的處理。
手動寫一個 CorsResponseHeaderFilter 的 GlobalFilter 去修改Response中的頭
@Component
public class CorsResponseHeaderFilter implements GlobalFilter, Ordered {
private static final Logger logger = LoggerFactory.getLogger(CorsResponseHeaderFilter.class);
private static final String ANY = "*";
@Override
public int getOrder() {
// 指定此過濾器位于NettyWriteResponseFilter之後
// 即待處理完響應體後接着處理響應頭
return NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER + 1;
}
@Override
@SuppressWarnings("serial")
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
exchange.getResponse().getHeaders().entrySet().stream()
.filter(kv -> (kv.getValue() != null && kv.getValue().size() > 1))
.filter(kv -> (kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN)
|| kv.getKey().equals(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS)
|| kv.getKey().equals(HttpHeaders.VARY)))
.forEach(kv ->
{
// Vary隻需要去重即可
if(kv.getKey().equals(HttpHeaders.VARY))
kv.setValue(kv.getValue().stream().distinct().collect(Collectors.toList()));
else{
List<String> value = new ArrayList<>();
if(kv.getValue().contains(ANY)){ //如果包含*,則取*
value.add(ANY);
kv.setValue(value);
}else{
value.add(kv.getValue().get(0)); // 否則預設取第一個
kv.setValue(value);
}
}
});
}));
}
}
此處有兩個地方要注意:
- 根據下圖可以看到,在取得傳回值後,Filter的Order 值越大,越先處理Response,而真正将Response傳回到前端的,是 NettyWriteResponseFilter, 我們要想在它之前修改Response,則Order 的值必須比NettyWriteResponseFilter.WRITE_RESPONSE_FILTER_ORDER 大。
修改後置filter時,網上有些部落格使用的是 Mono.defer去做的,這種做法,會從此filter開始,重新執行一遍它後面的其他filter,一般我們會添加一些認證或鑒權的 GlobalFilter ,就需要在這些filter裡用ServerWebExchangeUtils.isAlreadyRouted(exchange) 方法去判斷是否重複執行,否則可能會執行二次重複操作,是以建議使用fromRunnable 避免這種情況。
參考:
https://github.com/spring-cloud/spring-cloud-gateway/issues/728