生产上上线了展示能力输出功能,此功能将把内部的H5页面,在其他APP可以嵌入,涉及到跨域访问的问题
进行总结
参考文档:
跨域资源共享 CORS 详解
跨域的那些事儿
1.什么是跨域
别笑,之前我还真不知道
什么是跨域?
跨域,指的是浏览器不能执行其他网站的脚本。它是由浏览器的同源策略造成的,是浏览器施加的安全限制。
所谓同源是指,域名,协议,端口均相同,不明白没关系,举个栗子:
http://www.123.com/index.html 调用 http://www.123.com/server.php (非跨域)
http://www.123.com/index.html 调用 http://www.456.com/server.php (主域名不同:123/456,跨域)
http://abc.123.com/index.html 调用 http://def.123.com/server.php (子域名不同:abc/def,跨域)
http://www.123.com:8080/index.html 调用 http://www.123.com:8081/server.php (端口不同:8080/8081,跨域)
http://www.123.com/index.html 调用 https://www.123.com/server.php (协议不同:http/https,跨域)
请注意:localhost和127.0.0.1虽然都指向本机,但也属于跨域。
浏览器执行javascript脚本时,会检查这个脚本属于哪个页面,如果不是同源页面,就不会被执行。
2.浏览器的同源策略
同源策略又分为以下两种
- DOM同源策略:禁止对不同源页面DOM进行操作。这里主要场景是iframe跨域的情况,不同域名的iframe是限制互相访问的。
XmlHttpRequest同源策略:禁止使用XHR对象向不同源的服务器地址发起HTTP请求。 - 只要协议、域名、端口有任何一个不同,都被当作是不同的域,之间的请求就是跨域操作。
3.为什么我们需要跨域限制
主要是出于安全的考虑
AJAX同源策略主要用来防止CSRF攻击。如果没有AJAX同源策略,相当危险,我们发起的每一次HTTP请求都会带上请求地址对应的cookie,那么可以做如下攻击:
- 用户登录了自己的银行页面 http://mybank.com,http://mybank.com向用户的cookie中添加用户标识。
- 用户浏览了恶意页面 http://evil.com。执行了页面中的恶意AJAX请求代码。
- http://evil.com向http://mybank.com发起AJAX HTTP请求,请求会默认把http://mybank.com对应cookie也同时发送过去。
- 银行页面从发送的cookie中提取用户标识,验证用户无误,response中返回请求数据。此时数据就泄露了。
- 而且由于Ajax在后台执行,用户无法感知这一过程。
DOM同源策略也一样,如果iframe之间可以跨域访问,可以这样攻击:
- 做一个假网站,里面用iframe嵌套一个银行网站 http://mybank.com。
- 把iframe宽高啥的调整到页面全部,这样用户进来除了域名,别的部分和银行的网站没有任何差别。
- 这时如果用户输入账号密码,我们的主网站可以跨域访问到http://mybank.com的dom节点,就可以拿到用户的输入了,那么就完成了一次攻击。
4.如何做到合理的跨域访问---CORS
理论基础:CORS:”跨域资源共享”(Cross-origin resource sharing),这是一个W3C标准
CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信
- CORS 机制
浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。
具体可参考开头的阮一峰的参考文献
简要来讲,非简单请求多了一个预检的操作
4.1 预检会是一个OPTIONS的请求,例如
OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...`
4.2 服务器需要对预检请求进行回应
服务器收到"预检"请求以后,检查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应。
> HTTP/1.1 200 OK
> Date: Mon, 01 Dec 2008 01:15:39 GMT
> Server: Apache/2.0.61 (Unix)
> Access-Control-Allow-Origin: http://api.bob.com
> Access-Control-Allow-Methods: GET, POST, PUT
> Access-Control-Allow-Headers: X-Custom-Header
> Content-Type: text/html; charset=utf-8
> Content-Encoding: gzip
> Content-Length: 0
> Keep-Alive: timeout=2, max=100
> Connection: Keep-Alive
> Content-Type: text/plain
如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
(1)Access-Control-Allow-Methods
该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。
(2)Access-Control-Allow-Headers
如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
(3)Access-Control-Allow-Credentials
该字段与简单请求时的含义相同。
(4)Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
4.3 浏览器的正常请求和回应
一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
下面是"预检"请求之后,浏览器的正常CORS请求。
PUT /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com
X-Custom-Header: value
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...
上面头信息的Origin
字段是浏览器自动添加的。
下面是服务器正常的回应。
> Access-Control-Allow-Origin: http://api.bob.com
> Content-Type: text/html; charset=utf-8
特别注意!!!
上面头信息中,Access-Control-Allow-Origin
字段是每次回应都必定包含的。
5 作为服务器开发者,我们到底怎么做的
5.1 我们需要做什么
可以从上文的流程中看到,服务器的开发者,需要做的时候分2步
1.在预检请求中,添加信息,并返回200/204
204是一个没有响应体的成功响应2.在后续的请求中,继续添加Access-Control的信息
5.2 实际操作
生产我们用Nginx作为反向代理,所以我们需要再Nginx进行处理
- 一个普通的Nginx配置
server {
listen 80; #监听80端口,可以改成其他端口
server_name localhost; # 当前服务的域名
location ~ *.json {
proxy_pass 你的服务器;
}
我们需要处理逻辑的条件是2个,域名+是否为opation请求
所以我们Nginx需要对这2个条件进行判断,本来是很简单的事情,但
!!!nginx不支持多重判断
所以,配置成了这样
server {
listen 80; #监听80端口,可以改成其他端口
server_name localhost; # 当前服务的域名
location ~ *.json {
proxy_pass 你的服务器;
set $flag 0;
if ($http_origin ~ (域名A| 域名B)){
set $flag "${flag}1";
}
if ($request_method = 'OPTIONS'){
set $flag "${flag}2";
}
if ( $flag = "012" ){
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'X-PINGOTHER,Content-Type,Accept,Origin,User-Agent,Cache-Control,isOutput';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
return 204;
}
if ( $flag = "01" ){
add_header 'Access-Control-Allow-Origin' '$http_origin';
add_header 'Access-Control-Allow-Credentials' 'true';
add_header 'Access-Control-Allow-Headers' 'X-PINGOTHER,Content-Type,Accept,Origin,User-Agent,Cache-Control,isOutput';
add_header 'Access-Control-Allow-Methods' 'GET,POST,OPTIONS';
}
}
千万别忘了,opation预检请求后,仍然要添加信息,只是不需要直接返回204而已
6.跨域和CDN的坑
当一切在开发环境和测试环境验证无误后,上生产发现了新的问题。
以A域名为例,已经在Nginx上配置了A域名可以跨域
问题的发现:
生产验证环节,发现A域名下部分网址可以正常响应,部分网址无法正常相应。
正常跨域请求的Options请求和Get请求都可以获得服务器配置的请求头,有Access-Control各个字段,而不正常的请求,只有Options请求有,Get请求没有,怀疑是Get请求没有达到服务器6.1 第一步
观察后发现,不能正常响应的请求,都是.sjson结尾,此结尾代表着和CDN厂商规定的静态接口。因此怀疑是CDN的问题。
将不好的请求,改变Url的某个时间戳参数,请求正常执行。因此CDN是肯定是原因之一
- 6.2第二步
如果问题都在CDN上,则是因为请求到CDN层直接击中缓存的原因,没有达到服务器层获得最新的跨域配置
继续观察:在 .sjson结尾的请求中,也有部分参数请求是好的,部分请求参数是不好的
那么第二个问题来了,请求要么全是好的,要么全是不好的,为何会有概率的发生这种情况
因此怀疑是部分Nginx配置有误,进行排查,发现所有服务器都配置都已正常替换,服务器重新时间都在版本当晚
(这里因为CDN的缓存机制不了解,因此思考出现了问题,就卡住了)
- 6.3 第三步
联系CDN服务厂商,了解了CDN的推送策略有2种,公司的默认是第二种
一是物理上直接将目录下资源删除
二是将CDN目录下的资源置为过期,CDN会回源,比较新的资源是否变化,如果没变化,则继续使用,有变化,则更新
当时立马让厂商进行了第一种方式的删除,而没有经过分析,第二次错过了发现问题的机会。
厂商删除目录下资源后,再次访问,发现.sjson 的请求,还是部分正常 部分不正常
6.4第四步
陷入僵局后,再次搜寻资料,病急乱投医,包括怀疑CDN节点不同步等等。
这里经CDN厂商提醒,跨域请求的网址,正常不跨域的请求也会访问,2份缓存是同一份,可能存在这个问题,这样就可以解释为什么部分请求成功,部分不成功的问题。因为清楚缓存后,哪个请求先到,就会缓存哪份6.5五.如何验证
厂商同事,用有问题的请求,分别发送了带Origin字段和不带Origin的字段,用md5计算返回报文,比较发现一致。确认是同一份缓存。6.6六.如何解决
一是前端特殊处理,跨域的请求,加特殊的字段参数,但这需要修改代码
二是CDN厂商提供的,根据http请求头里的Origin字段,为每个值维护一份缓存
最终选择了第二个方案6.7继续测试
为了不影响生产,对第二个方案进行测试
厂商提供了一个配置了新的规则的测试CDN节点
比较请求:
curl -vo ~/tmp 'https://36.250.240.133/*****.sjson?*****&updTs=20180706004105' -H "Host:访问的域名" -k -H "Origin: 跨域的域名"
curl -vo ~/tmp 'https://36.250.240.133/*****.sjson?*****&updTs=20180706004105' -H "Host:访问的域名" -k -H "Origin: 不跨域的域名"
这样就是2份缓存了,可以比较返回的报文,最终解决了问题。
- 6.8反思点
这里有对CDN缓存机制不了解的原因,虽然知道CDN回源策略有定时,也有Url改变,但没有想到,这个请求头里的参数是不比较的。同样,以后CDN的接口如果有cookie等请求头信息,都要注意。不过静态请求也不应该包含那些变化的东西。
在了解了CDN推送的策略时,其实就应该想到过期后,CDN回源比较没更新,说明了请求的返回报文就是和跨域配置改变前是一样的,这就是同一份缓存