最常见到的跨域「问题」是这样:
a.com
和一个域名b.com
a.com
上有一个接口a.com/api
,会返回一些数据b.com
域名下的一个页面上访问a.com/api
得到数据直觉来讲这是一件挺奇怪的事情,我把上面的例子换成一个更实际的:
zhuanlan.zhihu.com
www.zhihu.com
,用户数据的api就在这个域名下www.zhihu.com
下的api(假如知乎后端没有做跨域的配置)
因为在web交互的环境中,只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
想象这样一个场景,如果世界上没有跨域限制,这时假如:
https://alipay.com/api/withdraw/?to_user=kindJeff&amout=1000
a.com
alipay.com
域名的cookiea.com
,打开页面,于是在你不知情的情况下发出了post请求,你的钱就被转到我的账号里了这就是跨站请求伪造(CSRF)。这是一个非常严重的后果,其利用的就是网站(上例的支付宝)对浏览器的充分信任。
所以浏览器一定会设置跨域限制,避免在用户和网站不知情的情况下发出请求。
回忆一下上面zhuanlan.zhihu.com
和www.zhihu.com
的例子,我只是想要GET
一些数据而已。
当遇到这种跨域问题不知怎么解决的时候,查询一下,会发现有两种解决办法:
zhuanlan.zhihu.com
想访问zhihu.com
的api,那可以在发请求前设置一下document.domain
的值为zhihu.com
。毕竟是子域,浏览器几乎没有做什么限制。(如我在一个油猴脚本里就这样用过https://greasyfork.org/zh-CN/scripts/27195)zhuanlan.zhihu.com
和www.zhihu.com
(或者a.com
和b.com
),就是被浏览器当作两个不同的域名的,一般就会使用JSONP了JSONP本质上就是让数据变成js代码,使用script标签来加载数据。
如我的用户数据api本来是https://www.zhihu.com/api/v4/members/kindjeff
,返回值为{"name": "kindJeff", "gender": 1}
想要通过JSONP实现在zhuanlan.zhihu.com
下跨域拿到这个数据,需要做的是:
https://www.zhihu.com/api/v4/members/kindjeff?callback=render
,得到的响应会是render({"name": "kindJeff", "gender": 1})
<script src="https://www.zhihu.com/api/v4/members/kindjeff?callback=render"></script>
因为浏览器并不限制script标签的src是要从哪里加载脚本,跨域问题似乎就被JSONP「绕过」了。
再想一想,浏览器不做script来源的跨域限制,而且大家都喜欢用JSONP并且改造了大量的api响应,问题不是回到了原点吗?我有一个网站a.com
,在里面嵌入了支付宝某个api的JSONP版本(也就是个script);我骗你访问a.com
;浏览器自动去加载script,也就去访问了这个api。
其实问题并没有回到原点,因为JSONP实际上受限很大。作为一个script标签,一是浏览器只会使用GET
方法去请求它,二是请求它的时候不会携带cookie,三是能被改造成JSONP形式的api一定是纯粹用来GET
数据的。
就算其他网站用这些JSONP改造过的接口,也不会对网站造成影响。
(所以后端开发者最好不要在GET
操作里做非幂等的事,因为别人在他的网站里嵌入script或者img标签放你网站的url,浏览器就会发出一个不带cookie的GET请求)
那更复杂的跨域需求应该怎么办呢?比如我就是需要在zhuanlan.zhihu.com
页面下,发post请求到www.zhihu.com
域名下的api,而且还是要带cookie的。这时JSONP就完全用不上了。
欢迎来到没有JSONP的世界。
我个人不喜欢用JSONP:一是因为JSONP是一种HACK,一种非标准行为,利用了script来做数据的事;二是它使得别人能直接在他的网页上使用你的数据(虽然还是阻止不了别人用一些后端代理的手段来获取数据,但至少能增加对方的成本)。
对于跨域的访问控制,是有HTTP标准的。这也是网上很多讲跨域的文章的主要内容,我就只简单介绍,跨域资源共享(CORS)把跨域行为分三类:
简单请求
如简单的GET
和POST
。
还是以zhuanlan.zhihu.com
页面请求www.zhihu.com
的api为例。
Origin
头,值为zhuanlan.zhihu.com
Access-Control-Allow-Origin
字段包含(匹配)了发送请求的页面所在的域名(zhuanlan.zhihu.com
),浏览器就会认为合法,把数据接着使用。这样的好处很明显:我只需要在服务器端(通常是网关这一层)配置好Access-Control-Allow-Origin
,而我的代码逻辑不需要对来源站点区别对待,就阻止其他人纯前端的手段使用我的数据,做到HTTP访问控制。
预检请求
略微复杂一定的请求,如PUT
和DELETE
等,或者请求时添加了CORS安全的header之外的header(如自定义的)。
这时,正式发送跨域请求前,浏览器会先对目标api发出一个OPTIONS
预检请求,这个请求里会带三个和跨域相关的header,其值为预检之后,正式发送api请求时将会使用的来源/方法/请求头。这三个header是:
看名称应该能大概知道对应什么了。预检请求的响应需要带着与它们对应匹配的header和值,这样浏览器才会去请求跨域api。
预检请求的出现,是因为PUT
等复杂操作通常是非幂等的。如果像简单请求一样直接请求,发现响应不合理才去拦截响应值,这个时候后端的PUT
操作里该执行的事情已经被执行过了。
(至于为什么POST
这个非幂等语义的方法会是简单请求,我觉得应该是历史包袱。毕竟在CORS出现前,form表单里POST
就是能跨域使用的。而早期的js很弱小,提交form之后页面会刷新跳转到目标地址,源地址是拿不到POST
响应的数据的)
带cookie的请求
这种跨域请求才是最危险的,最严重情况下能实现上面举的支付宝转账例子。
所以这种请求要求响应头里Access-Control-Allow-Credentials
为true,且Access-Control-Allow-Origin
不能是通配符,防止后端开发者犯错。
关于CORS更具体的规则,可以在MDN查阅到详细的资料。
按照上面的规则,支付宝把CORS设置的非常详细和安全,在自己同公司的业务能访问支付宝接口的同时,让a.com
这种网站再无可乘之机,没有办法跨域访问。
这时,你登上a.com
,点了a.com
网站上的一个按钮。你的钱还是消失了。
这就是点击劫持(clickjacking)。
实现原理可以如下:
iframe
放在a.com
的网页上iframe
设置为透明,在它的按钮位置下面放置一个可以看见的「下一页」按钮这种「跨域」也有类似CORS的控制方式,即X-Frame-Options
响应头。它的值有三种:
发现网页在iframe里,且X-Frame-Options
响应头的值不符合要求,浏览器不会加载这个iframe。