专栏首页Linyb极客之路为什么给你设置重重障碍?讲一讲Web开发中的跨域

为什么给你设置重重障碍?讲一讲Web开发中的跨域

一、跨域是个什么「问题」?

最常见到的跨域「问题」是这样:

  • 我有一个域名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交互的环境中,只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

想象这样一个场景,如果世界上没有跨域限制,这时假如:

  • 支付宝的转账操作是一个post请求,大概是https://alipay.com/api/withdraw/?to_user=kindJeff&amout=1000
  • 我写了一段ajax的post请求代码,请求连接是上面的url。然后我把这段代码嵌入我的网站a.com
  • 你不久前登陆过支付宝,浏览器里保存了alipay.com域名的cookie
  • 我让你访问a.com,打开页面,于是在你不知情的情况下发出了post请求,你的钱就被转到我的账号里了

这就是跨站请求伪造(CSRF)。这是一个非常严重的后果,其利用的就是网站(上例的支付宝)对浏览器的充分信任。

所以浏览器一定会设置跨域限制,避免在用户和网站不知情的情况下发出请求。

三、JSONP——最常用的绕过办法

回忆一下上面zhuanlan.zhihu.comwww.zhihu.com的例子,我只是想要GET一些数据而已。

当遇到这种跨域问题不知怎么解决的时候,查询一下,会发现有两种解决办法:

  • 如果是子域名下的页面想访问父域的api,如zhuanlan.zhihu.com想访问zhihu.com的api,那可以在发请求前设置一下document.domain的值为zhihu.com。毕竟是子域,浏览器几乎没有做什么限制。(如我在一个油猴脚本里就这样用过https://greasyfork.org/zh-CN/scripts/27195)
  • 在非父子域关系的情况下,如zhuanlan.zhihu.comwww.zhihu.com(或者a.comb.com),就是被浏览器当作两个不同的域名的,一般就会使用JSONP了

JSONP本质上就是让数据变成js代码,使用script标签来加载数据。

如我的用户数据api本来是https://www.zhihu.com/api/v4/members/kindjeff,返回值为{"name": "kindJeff", "gender": 1}

想要通过JSONP实现在zhuanlan.zhihu.com下跨域拿到这个数据,需要做的是:

  • 改造这个api,让他返回一段js代码而不是json数据。如改造成,你访问https://www.zhihu.com/api/v4/members/kindjeff?callback=render,得到的响应会是render({"name": "kindJeff", "gender": 1})
  • 在专栏文章页面,不使用ajax去拿取数据,而是嵌入一个script标签:<script src="https://www.zhihu.com/api/v4/members/kindjeff?callback=render"></script>
  • 在专栏文章页面写好一个叫render的函数,用来渲染用户数据

因为浏览器并不限制script标签的src是要从哪里加载脚本,跨域问题似乎就被JSONP「绕过」了。

四、为什么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就完全用不上了。

五、跨域资源共享(CORS)

欢迎来到没有JSONP的世界。

我个人不喜欢用JSONP:一是因为JSONP是一种HACK,一种非标准行为,利用了script来做数据的事;二是它使得别人能直接在他的网页上使用你的数据(虽然还是阻止不了别人用一些后端代理的手段来获取数据,但至少能增加对方的成本)。

对于跨域的访问控制,是有HTTP标准的。这也是网上很多讲跨域的文章的主要内容,我就只简单介绍,跨域资源共享(CORS)把跨域行为分三类:

简单请求

如简单的GETPOST

还是以zhuanlan.zhihu.com页面请求www.zhihu.com的api为例。

  • 浏览器发出请求时,request里会带上Origin头,值为zhuanlan.zhihu.com
  • 这时只需要api响应header里带的Access-Control-Allow-Origin字段包含(匹配)了发送请求的页面所在的域名(zhuanlan.zhihu.com),浏览器就会认为合法,把数据接着使用。
  • 否则,浏览器会拦截掉这段数据:没错,响应的数据已经放body里到达了客户端,而浏览器会阻止掉,让专栏页面里负责发ajax的那段js代码拿不到响应值。

这样的好处很明显:我只需要在服务器端(通常是网关这一层)配置好Access-Control-Allow-Origin,而我的代码逻辑不需要对来源站点区别对待,就阻止其他人纯前端的手段使用我的数据,做到HTTP访问控制。

预检请求

略微复杂一定的请求,如PUTDELETE等,或者请求时添加了CORS安全的header之外的header(如自定义的)。

这时,正式发送跨域请求前,浏览器会先对目标api发出一个OPTIONS预检请求,这个请求里会带三个和跨域相关的header,其值为预检之后,正式发送api请求时将会使用的来源/方法/请求头。这三个header是:

  • Origin
  • Access-Control-Request-Method
  • Access-Control-Request-Headers

看名称应该能大概知道对应什么了。预检请求的响应需要带着与它们对应匹配的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)。

实现原理可以如下:

  • 假如支付宝有一个页面,页面上的按钮点击是转账1000元给kindJeff
  • 我把这个页面作为一个iframe放在a.com的网页上
  • 我把这个iframe设置为透明,在它的按钮位置下面放置一个可以看见的「下一页」按钮
  • 你看见我的网页,毫无防备地点击了下一页,实际上点击的位置是转账按钮

这种「跨域」也有类似CORS的控制方式,即X-Frame-Options响应头。它的值有三种:

  • DENY。表示该页面不允许在 frame 中展示,即便是在相同域名的页面中嵌套也不允许。
  • SAMEORIGIN。表示该页面可以在相同域名页面的 frame 中展示。
  • ALLOW-FROM uri。表示该页面可以在指定来源(uri)的 frame 中展示。

发现网页在iframe里,且X-Frame-Options响应头的值不符合要求,浏览器不会加载这个iframe。

本文分享自微信公众号 - Linyb极客之路(gh_c420b2cf6b47),作者:kindJeff

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2018-08-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 通俗易懂的Nginx工作原理

    反向代理(Reverse Proxy)方式是指以代理服务器来接受internet上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给...

    lyb-geek
  • 一位Java工程师的阶段性工作总结

    1.1.1、通常的模块分布:一般如果你要实现一个web应用,你从后台将数据展示到前端页面,在一个比较大的公司,你少不了跟其他项目有交集(你调用他的接口,他依赖你...

    lyb-geek
  • SpringBoot使用Sharding-JDBC分库分表

    有关Sharding-JDBC介绍这里就不在多说,之前Sharding-JDBC是当当网自研的关系型数据库的水平扩展框架,现在已经捐献给Apache,具体可以查...

    lyb-geek
  • linux 终端下最简单的代理方式(proxychains)

    我以前写过给linux终端设置代理 这个是用polipo这个工具把socks5代理转换成为http和https代理来实现终端下代理的,那么终端下有没有原生的使...

    bboysoul
  • 业界 | 谷歌新进展:用DNN模型为YouTube视频添加环境音效字幕

    我们在感知外部世界的过程中,声音(audio)起到了极大的作用。在这里,我们把声音分解为两类,一类是语音(speech),另一类是环境音(sound)。人们会本...

    AI科技评论
  • 前端必备技能:json-server全攻略

    由于json-server需要通过Node对其进行启动,所以首先要安装Node。Node可自行安装,在此不再进行演示。

    用户1272076
  • 虎扑融资6.18亿元 官网域名超吸睛

    近日虎扑完成新一轮6.18亿融资,领投方是“国家队”中金公司。据公司透露,过去两年低调的虎扑依托海量互联网用户,在电商变现,体育IP开发,以及产业...

    躲在树上的域小名
  • 在知乎上学 Python - 爬虫篇

    知乎是个好地方。虽然近年来,为了吸引更多的用户,知乎的定位与早期略有点偏离。但从内容质量和专业性来说,知乎仍然是国内数一数二的知识型社区。不少同学都是通过知乎发...

    Crossin先生
  • Sound of silence: 数据传输的小众黑科技

    上周面试了一个来自俄罗斯的 android 工程师。很 geek,对 office 的 binary format(不是后来的 xml format)做过深入的...

    tyrchen
  • 快速学习ES6-操作索引

    Elasticsearch也是基于Lucene的全文检索库,本质也是存储数据,很多概念与MySQL类似的。

    cwl_java

扫码关注云+社区

领取腾讯云代金券