URL
date
AI summary
slug
status
tags
summary
type

跨域基础知识

跨域相关的基础知识,阮一峰老师的文章已经非常全面了。建议还不太了解的同学必须要先去看看。https://www.ruanyifeng.com/blog/2016/04/cors.html

我碰到的跨域问题

spring-cloud-gateway + downstream

 
我们应该是从2年开始,开始尝试使用spring-cloud-gateway。增加一层gateway后整体架构如下:
notion image
其中,a-web是需要支持跨域访问的,所以在a-web里已经有相关的跨域配置:
@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); config.addAllowedOrigin("*"); config.setAllowCredentials(true); config.addAllowedMethod("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource(); configSource.registerCorsConfiguration("/**", config); return new CorsFilter(configSource); } }
不过在引入gateway之后,跨域出现了问题:
Access to fetch at '<http://domain/api/v1/brand/query-page?currentPage=1&pageSize=10&sign=a66f47c8c898fa72b7d1ad222ae79479>' from origin '<https://www.baidu.com>' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.
不过,这里只有需要发起预检(Preflight)的复杂请求会报错,在预检请求里就会报跨域异常。简单请求是可以正常响应的。看了源代码发现gateway会拦截预检请求并处理,不会发送到下游
notion image
而我们最初在gateway这一层是没有配置跨域的,于是尝试在gateway这一层也增加跨域配置
spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedOrigins=* spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedMethods=* spring.cloud.gateway.globalcors.cors-configurations.[/**].allowedHeaders=* spring.cloud.gateway.globalcors.cors-configurations.[/**].allowCredentials=true spring.cloud.gateway.globalcors.cors-configurations.[/**].maxAge=3600
不过,配置之后,跨域请求还是没能成功,这次又抛出一个新的异常:
The ‘Access-Control-Allow-Origin’ header contains multiple values “*, *”, but only one is allowed.
正常的跨域响应头里应该只包含一个Access-Control-Allow-Origin,而我们的响应头里面包含了2个。原因就是a-web和gateway都有跨域的配置。a-web处理完请求之后已经在响应头上加了Access-Control-Allow-Origin然后返回给gateway,而gateway没有兼容处理响应头里已经有Access-Control-Allow-Origin的场景。所以导致了响应头里有多个Access-Control-Allow-Origin
这个问题网上有很多文章都提到了,也提供了两种思路去修改:
  1. 通过自定义GlobalFilter来手动移除下游服务端添加的Access-Control-Allow-Origin
  1. 通过spring-cloud-gateway自带的多个相同响应头的处理机制,配置保留策略,保留一个头即可
而我们采用了一种更加简单的方式去解决这个问题:让下游的服务端能识别出这个请求是通过gateway过来的,还是没经过gateway直接访问的。只有在处理直接访问的请求时,下游才需要加上跨域的逻辑。于是我们自定义了一个新的CustomerCorsFilter,用来替换前面的CorsFilter:
public class CustomerCorsFilter extends CorsFilter { /** * Constructor accepting a {@link CorsConfigurationSource} used by the filter to find the {@link * CorsConfiguration} to use for each incoming request. * * @see UrlBasedCorsConfigurationSource */ public CustomerCorsFilter(CorsConfigurationSource configSource) { super(configSource); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 】 String fromGateway = request.getHeader(Constants.REQUEST_HEADER_TRACE_ID); if (StringUtils.isBlank(fromGateway)) { super.doFilterInternal(request, response, filterChain); } else { filterChain.doFilter(request, response); } } }

spring-cloud-gateway + spring-cloud-gateway

不过,又一个场景的出现,让我们不得不使用其他方式来解决这个问题。引入spring-cloud-gateway之后,我想把整体架构升级,把xxx-web这一层废弃,web接口直接下沉到对应的领域服务里。gateway可以通过url直接提取到路由信息,所以必须规范url的命名。但是之前已经定义好的那些“不规范”的url,我们需要通过自定义转发规则来把它规范化。
这里还要提到我们的灰发环境:我们的灰发是通过生产环境的gateway来做的,灰发的服务会以服务名称_pre的形式注册到gateway的注册中心。请求到达生产gateway后,会根据请求的特征(比如ip、用户id等)做路由计算,判断是走灰发还是走生产。但是当我们想要灰发上面提到的转发规则的时候,这条路就行不通了。我们必须要把这套转发规则配置在灰发环境。
所以,“软灰发”行不通了,我们只能来点硬的。于是增加一种硬灰发的策略——识别出需要走灰发的请求,直接把它转发到灰发环境的域名,走完整的灰发slb->灰发gateway的路径。而这样通过gateway到gateway再到底层服务的路径,就暴露了之前的问题,我们无法避免在gateway这一层做调整了。于是我们采用了自定义GlobalFilter的方式来删除下游添加的跨域响应header
public class CorsFixResponseHeaderFilter implements GlobalFilter, Ordered { @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))) .forEach(kv -> { kv.setValue(new ArrayList<String>() {{ add(kv.getValue().get(0)); }}); }); })); } }
具体参考的下面这篇文章,应该是当时我看到的关于这个内容写得最好的一篇文章了:

跨域白名单

由于前面一直图省事,配置的都是*,但是一些安全机构要求不允许跨域通配,所以只能改成跨域白名单。但是我们一个gateway支持了很多域名,找前端同学都无法全部枚举出来,只能通过打开日志收集origin头。
收集了一段时间,差不多有100+个域名(因为我们是平台型的供应商,所以给不少租户都提供了子域名)。如果这些子域名都要一个个手动配的话,就非常麻烦,并且后续如果要增加子域名的话还要多一步配置跨域的步骤,所以我们期望可以通过通配来配置跨域白名单。不过遗憾的是,spring-cloud-gateway暂时不支持跨域通配,只支持*。我们找到对应的跨域处理类CorsProcessor,稍微调整了一下逻辑:
public class CorsMatchProcessor extends DefaultCorsProcessor { private final AntPathMatcher matcher = new AntPathMatcher(); @Override protected String checkOrigin(CorsConfiguration config, String requestOrigin) { List<String> allowedOrigins = config.getAllowedOrigins(); Boolean allowCredentials = config.getAllowCredentials(); if (!StringUtils.hasText(requestOrigin)) { return null; } if (ObjectUtils.isEmpty(allowedOrigins)) { return null; } // 可以看到,原生的配置只判断了是否包含* if (allowedOrigins.contains(CorsConfiguration.ALL)) { if (allowCredentials != Boolean.TRUE) { return CorsConfiguration.ALL; } else { return requestOrigin; } } for (String allowedOrigin : allowedOrigins) { // 这里是我们扩展的支持通配的核心代码 if (matcher.match(allowedOrigin, requestOrigin)) { return requestOrigin; } if (requestOrigin.equalsIgnoreCase(allowedOrigin)) { return requestOrigin; } } return null; } }
更进一步的,我们结合了我们的配置中心做跨域配置的实时生效
public class CorsConfigListener implements CallbackMethodListener { @Resource private RoutePredicateHandlerMapping routePredicateHandlerMapping; @Resource private GlobalCorsProperties globalCorsProperties; @FocusOn(keys = {"spring.cloud.gateway.globalcors.*"}) @Override public boolean onChange(CallbackEvent callbackEvent) { log.info("cors config changed:{}", callbackEvent.getRelatedMap()); routePredicateHandlerMapping.setCorsConfigurations(globalCorsProperties.getCorsConfigurations()); return true; } }
最后,替换默认的CorsProcessor
@Configuration public class CustomCorsConfiguration { @Resource private RoutePredicateHandlerMapping routePredicateHandlerMapping; @PostConstruct void config() { routePredicateHandlerMapping.setCorsProcessor(new CorsMatchProcessor()); } @Bean public CorsConfigListener configListener(){ return new CorsConfigListener(); } }

前端代理请求也会涉及到跨域?

之前以为只有浏览器发起的请求才会有跨域一说(毕竟同源策略应该是浏览器的安全策略),这可能是一个错误的认知。其实只要你的请求里带了origin头,并且服务端有跨域的配置,服务端就会把这个请求当做一个跨域请求处理。所以,前端代理发起的请求或者是后端服务发起的请求,只要请求头里带了origin,那么就有可能跨域请求失败。
如果服务端没有配置跨域,默认其实是不会处理origin头的,也就是说,你带了origin头也能请求成功。但是,响应体里也不会有相应的支持跨域的头,如果你是通过浏览器发起的请求,那响应体里没带相应的跨域头,浏览器里自然就报错了,虽然这次请求到后端服务是成功的。
另外,前端代理还有一个比较迷惑的配置参数,叫做changeOrigin,乍一看,以为是修改origin头的,但是实际上是用来修改host头的。可以看看下面这篇文章

一个奇怪的options请求

我们知道,一个简单请求是不需要发起options请求的,比如下面这个:
<http://localhost:8080/api/v1/city/get-city?appVersion=0.0.224&lng=120128246&channel=WeChatTinyApplet&sign=b389e99ce44e2b9a5d6ffe272d5d0319&subChannel=wxapp_haoduo_interest_classes&lat=30267118>
但是,我们在chrome的console发起fetch请求时,发现它竟然发起了一个options请求
notion image
更为奇怪的是,在options请求跨域失败的情况下,竟然还发起了GET请求,并通过了跨域检查,得到了相应数据(我通过后台日志发现请求确实到达了downstream server,2个请求都有过去)
notion image
不知道是不是在https下请求http的原因导致的?我尝试在http里发起上面的请求,但是貌似被拦截了:
notion image
看起来好像是跟我localhost这个domain也有一定的关系,于是我做了内网穿透,换了个域名,发现此时是可以发起跨域请求的,并且也没有options请求,服务端也收到的请求并成功返回了。但是由于没有跨域配置所以被浏览器丢弃了
notion image
于是我尝试在https下也把跨域请求的域名从localhost替换成内网穿透的域名,发现这次请求由于是https->http的直接被浏览器block了
notion image
这里需要注意的是,在没有任何跨域配置的情况下,gateway只会拦截options请求,而对于非options的请求都会转发到下游去处理。所以你会看到,其实第二个请求成功了,只是响应体里没有对应的跨域头,所以被浏览器丢弃了

参考

  1. https://www.ruanyifeng.com/blog/2016/04/cors.html
  1. https://github.com/spring-cloud/spring-cloud-gateway/issues/728
  1. https://developer.aliyun.com/article/1041355
  1. https://blog.csdn.net/wanglele16/article/details/101547020
SpringBoot是如何做到一个jar包就可以直接运行的mysql流式查询下的性能隐患