请求中没有使用 ReadableStream 对象。
简单请求模式,浏览器直接发送跨域请求,并在请求头中携带Origin的头,表明这是一个跨域的请求。服务器端接到请求后,会根据自己的跨域规则,通过Access-Control-Allow-Origin和Access-Control-Allow-Methods响应头,来返回验证结果。
应答中携带了跨域头 Access-Control-Allow-Origin。使用 Origin 和 Access-Control-Allow-Origin 就能完成最简单的访问控制。本例中,服务端返回的 Access-Control-Allow-Origin: * 表明,该资源可以被任意外域访问。如果服务端仅允许来自 的访问,该首部字段的内容如下:
Access-Control-Allow-Origin: http://admin.training.com
现在,除了 ,其它外域均不能访问该资源。
2.2 预检请求
浏览器在发现页面发出的请求非简单请求,并不会立即执行对应的请求代码,而是会触发预先请求模式。预先请求模式会先发送preflight request(预先验证请求),preflight request是一个OPTION请求,用于询问要被跨域访问的服务器,是否允许当前域名下的页面发送跨域的请求。在得到服务器的跨域授权后才能发送真正的HTTP请求。
OPTIONS请求头部中会包含以下头部:
服务器收到OPTIONS请求后,设置头部与浏览器沟通来判断是否允许这个请求。
如果preflight request验证通过,浏览器才会发送真正的跨域请求。
3 后端配置
后端配置我尝试过两种方式,经过两个月的测试,都能非常稳定的运行。
▍MND推荐的Nginx配置
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;
}
}
在配置Access-Control-Allow-Headers属性的时候,因为自定义的header包含签名和token,数量较多。为了简洁方便,我把Access-Control-Allow-Headers配置成 * 。
在Chrome和firefox下没有任何异常,但在IE11下报了如下的错:
Access-Control-Allow-Headers 列表中不存在请求标头 content-type。
原来IE11要求预检请求返回的Access-Control-Allow-Headers的值必须以逗号分隔。
▍SpringBoot自带CorsFilter
首先基础框架里默认有如下跨域配置。
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("POST", "GET", "PUT", "OPTIONS", "DELETE")
.allowCredentials(true)
.allowedHeaders("*")
.maxAge(3600);
}
可是部署完成,进入还是报CORS异常:
从nginx和tomcat日志来看,仅仅收到一个OPTION请求,springboot应用里有一个拦截器ActionInterceptor,从header中获取token,调用用户服务查询用户信息,放入request中。当没有获取token数据时,会返回给前端JSON格式数据。
但从现象来看CorsMapping并没有生效。
为什么呢?实际上还是执行顺序的概念。下图展示了 过滤器,拦截器,控制器的执行顺序。
DispatchServlet.doDispatch()方法是SpringMVC的核心入口方法。
// Determine handler for the current request.
mappedHandler = getHandler(processedRequest);
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
那么CorsMapping在哪里初始化的呢?经过调试,定位于AbstractHandlerMapping。
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
chain.addInterceptor(new CorsInterceptor(config));
}
return chain;
}
代码里有预检判断,通过 PreFlightHandler.handleRequest() 中处理,但是处于正常的业务拦截器之后。
最终选择CorsFilter 主要基于两点原因:
private CorsConfiguration corsConfig() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOrigin("*");
corsConfiguration.addAllowedHeader("*");
corsConfiguration.addAllowedMethod("*");
corsConfiguration.setAllowCredentials(true);
corsConfiguration.setMaxAge(3600L);
return corsConfiguration;
}
@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig());
return new CorsFilter(source);
}
下面的代码里,allowHeader是通配符 * 的时候,CorsFilter在设置 Access-Control-Allow-Headers 的时候,会将 Access-Control-Request-Headers 以逗号拼接起来,这样就可以避免IE11响应头的问题。
public List checkHeaders(@Nullable List requestHeaders) {
if (requestHeaders == null) {
return null;
}
if (requestHeaders.isEmpty()) {
return Collections.emptyList();
}
if (ObjectUtils.isEmpty(this.allowedHeaders)) {
return null;
}
boolean allowAnyHeader = this.allowedHeaders.contains(ALL);
List result = new ArrayList(requestHeaders.size());
for (String requestHeader : requestHeaders) {
if (StringUtils.hasText(requestHeader)) {
requestHeader = requestHeader.trim();
if (allowAnyHeader) {
result.add(requestHeader);
}
else {
for (String allowedHeader : this.allowedHeaders) {
if (requestHeader.equalsIgnoreCase(allowedHeader)) {
result.add(requestHeader);
break;
}
}
}
}
}
return (result.isEmpty() ? null : result);
}
浏览器的执行效果如下:
4 preflight响应码:200 vs 204
后端配置完成之后,团队里的小伙伴问我:“勇哥,那预检请求返回的响应码到底是200还是204呀?”。这个问题真把我给问住了。
我司的API网关的预检响应码是200,CorsFilter预检响应码也是200。
MDN给的示例预检响应码全部是204。
我只能采取Google大法,赫然发现大名鼎鼎的API网关Kong的开发者也针对这个问题有一番讨论。
MDN曾经推荐的preflight响应码是200 ,所以Kong也和MDN同步成200;
The page was updated since then. See its contents on Sept 30th, 2018:
://developer.mozilla.org/en-US/docs/Glossary/Preflight_request
后来MDN将响应码修改204,于是Kong的开发者争论要不要和MDN保持同步。
争论的核心点在于:有没有迫切的必要。200响应码运行得很好,似乎也将永远正常运行下去。而更换成204,不确定是否有隐藏问题。
说到底,框架开发者还是依赖于浏览器的底层实现。在这个问题上,没有足够权威的资料能够支撑框架开发者,而各个知识点都散落在网络的各个角落,充斥着不完整的细节和部分解决方案,这些都让框架开发者非常困惑。
最后,Kong的源码里预检响应码仍然是200,并没有和MDN保持同步。
我仔细查看了各大主流网站,95%预检响应码是200。而经过两个多月的测试,Nginx配置预检响应码204,在主流的浏览器Chrome , Firefox , IE11 也没有出现任何问题。
所以,200 works everywhere, 而204在当前主流的浏览器里也得到非常好的支持。
5 Chrome: 非安全私有网络
本以为跨域问题就这样解决了。没想到还是有一个小插曲。
产品总监需要给客户做演示,我负责搞定演示环境。申请域名,准备阿里云服务器,应用打包,部署,一切都很顺利。
可是在公司内网访问演示环境,有一个页面一直报CORS报错,报错内容类似下图:
跨域的错误类型是:InsecurePrivateNetwork。
这和原来遇到的跨域错误完全不一样,我心里一慌。马上Google , 原来这是chrome更新到94之后新的特性,可以手工关闭这个特性。
打开 tab 页面 chrome://flags/#block-insecure-private-network-requests
将其 Block insecure private network requests 设置为 Disabled, 然后重启就行了, 这样子就相当于把这个功能禁用掉。
但这样是治标不治本呀。有点诡异的是,当我们不在公司内网访问演示环境的时候,演示环境完全正常,出错的页面也能正常访问。
仔细看官方的文档,CORS-RFC1918 指出如下三种请求会受影响。
这样,我把问题定位在这个出错的第三方接口地址上。公司很多产品都依赖这个接口服务。当在公司内网访问的时候,该域名映射地址类似:172.16.xx.xx。
而这个ip正好是rfc1918上规定的私有网络。
10.0.0.0 - 10.255.255.255 (10/8 prefix)
172.16.0.0 - 172.31.255.255 (172.16/12 prefix)
192.168.0.0 - 192.168.255.255 (192.168/16 prefix)
内网通过Chrome访问这个页面的时候,会触发非安全私有网络拦截。
如何解决呢?官方给出的方案分两步走:
限时特惠:本站每日持续更新海量各大内部网赚创业教程,会员可以下载全站资源点击查看详情
站长微信:11082411