请求跨域问题详解

  在JavaScript中,有一个很重要的安全性限制,被称为“Same-Origin Policy”(同源策略)。这一策略对于JavaScript代码能够访问的页面内容做了很重要的限制,即JavaScript只能访问与包含它的文档在同一域下的内容。

  💢跨越是浏览器进行安全限制的一种方法,如果浏览器禁用了这种安全限制就不会出现跨域问题产生跨域的原因(以下三者都满足):

  • 只要调用方访问被调用方的域名、端口、IP不一样
  • 浏览器没有禁用安全限制
  • 采用了XMLHttpRequest的请求

  JavaScript这个安全策略在进行多iframe或多窗口编程、以及Ajax编程时显得尤为重要。根据这个策略,在baidu.com下的页面中包含的JavaScript代码,不能访问在google.com域名下的页面内容;甚至不同的子域名之间的页面也不能通过JavaScript代码互相访问。对于Ajax的影响在于,通过XMLHttpRequest实现的Ajax请求,不能向不同的域提交请求,例如,在abc.example.com下的页面,不能向def.example.com提交Ajax请求,等等。

  为什么浏览器要实现同源限制?我们举例说明:

  比如一个黑客,他利用iframe把真正的银行登录页面嵌到他的页面上,当你使用真实的用户名和密码登录时,如果没有同源限制,他的页面就可以通过javascript读取到你的表单中输入的内容,这样用户名和密码就轻松到手了。又比如你登录了OSC,同时浏览了恶意网站,如果没有同源限制,该恶意网站就可以构造AJAX请求频繁在OSC发广告帖。

一、跨域问题发生场景

  

  • 特别注意两点:

  1、如果是协议和端口造成的跨域问题“前台”是无能为力的

  2、在跨域问题上,域仅仅是通过“URL的首部”来识别而不会去尝试判断相同的ip地址对应着两个域或两个域是否在同一个ip上。比如上面的,http://www.a.com/a.js和http://70.32.92.74/b.js。虽然域名和域名的ip对应,不过还是被认为是跨域。

  *“URL的首部”指window.location.protocol +window.location.host。其中,window.location.protocol:指含有URL第一部分的字符串,如http: ,window.location.host:指包含有URL中主机名:端口号部分的字符串.如//www.cenpok.net/server/

二、跨域问题的解决方案

  💢解决跨域的思路

  • 被调用方解决: 被调用方解决-支持跨域(根据http协议关于跨域方面的要求,增加响应头信息,告诉浏览器允许被跨域调用)(因为在发生跨域请求时首先调用方发送一个预检请求(OPTIONS请求),这个请求就会被带上允许跨越的请求头信息)
  • 调用方解决:使用代理做调用解决跨域问题-隐藏跨域(利用nginx的反向代理,使访问同一个域名不同的资源路径会代理到不同的服务器上,每个跨域的请求都会带上origin请求头字段,因为访问的资源都是同域名下的,所以不会产生跨越问题)

1、JSONP跨域

​   JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响,而利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。所有,通过Chrome查看所有JSONP发送的Get请求都是js类型,而非XHR。 

    

①原理

  我们知道,在页面上有三种资源是可以与页面本身不同源的。它们是:js脚本,css样式文件,图片,像淘宝等大型网站,肯定会将这些静态资源放入cdn中,然后在页面上连接,如下所示,所以它们是可以链接访问到不同源的资源的。

1 <script type="text/javascript" src="某某cdn地址" ></script>
2 <link type="text/css" rel="stylesheet" href="某个cdn地址" />
3 <img src="某个cdn地址" alt=""/>

  而jsonp就是利用了script标签的src属性是没有跨域的限制的,从而达到跨域访问的目的。因此它的最基本原理就是:动态添加一个<script>标签来实现。

②实现方法:

  这里是使用ajax来请求的,看起来和ajax没啥区别,其实还是有区别的。ajax的核心是通过XmlHttpRequest获取非本页内容,而jsonp的核心则是动态添加<script>标签来调用服务器提供的js脚本。

复制代码
$.ajax({  
        url:"http://www.baidu.com/service",  
        dataType:'jsonp',  
        data:'',  
        jsonp:'callback',  
        success:function(data) {  
            // some code
        }  
    });  
复制代码

  上面的代码中,callback是必须的,callback是什么值要跟后台拿。获取到的jsonp数据格式如下:

callback({
    "code": "CA1998",
    "price": 1780,
    "tickets": 5
});

 ③JSONP的不足之处:

  1. 只能使用get方法,不能使用post方法:我们知道 script,link, img 等等标签引入外部资源,都是 get 请求的,那么就决定了 jsonp 一定是 get 的。但有时候我们使用的 post 请求也成功,为啥呢?这是因为当我们指定dataType:'jsonp',不论你指定:type:"post" 或者type:"get",其实质上进行的都是 get 请求!
  2. 没有关于 JSONP 调用的错误处理。如果动态脚本插入有效,就执行调用;如果无效,就静默失败。失败是没有任何提示的。例如,不能从服务器捕捉到 404 错误,也不能取消或重新开始请求。不过,等待一段时间还没有响应的话,就不用理它了。

2、跨域资源共享 CORS

​   Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法,以避开浏览器的同源策略,确保安全的跨域数据传输。现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。

  浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。由于该类跨域方案使用较为广泛,本篇将详细介绍。

2.1简单请求

  只要同时满足以下两大条件,就属于简单请求。

(1)请求方法是以下三种方法之一:

1 HEAD
2 GET
3 POST

(2)HTTP的头信息不超出以下几种字段:

1 Accept
2 Accept-Language
3 Content-Language
4 Last-Event-ID
5 Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

2.1.1基本流程

  对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。

1 GET /cors HTTP/1.1
2 Origin: http://api.bob.com
3 Host: api.alice.com
4 Accept-Language: en-US
5 Connection: keep-alive
6 User-Agent: Mozilla/5.0...

  上面的头信息中,Origin字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。

  如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。

  如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

1 Access-Control-Allow-Origin: http://api.bob.com
2 Access-Control-Allow-Credentials: true
3 Access-Control-Expose-Headers: FooBar
4 Content-Type: text/html; charset=utf-8

  上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。

(1)Access-Control-Allow-Origin

  该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。

(2)Access-Control-Allow-Credentials

  该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。

(3)Access-Control-Expose-Headers

  该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

2.1.2 withCredentials 属性

  上面说到,CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

1 Access-Control-Allow-Credentials: true

  另一方面,开发者必须在AJAX请求中打开withCredentials属性。

1 var xhr = new XMLHttpRequest();
2 xhr.withCredentials = true;

  否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials

1 xhr.withCredentials = false;

  需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

2.2非简单请求

2.2.1 预检请求

  非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

  非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

  下面是一段浏览器的JavaScript脚本。

1 var url = 'http://api.alice.com/cors';
2 var xhr = new XMLHttpRequest();
3 xhr.open('PUT', url, true);
4 xhr.setRequestHeader('X-Custom-Header', 'value');
5 xhr.send();

  上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。

1 OPTIONS /cors HTTP/1.1
2 Origin: http://api.bob.com
3 Access-Control-Request-Method: PUT
4 Access-Control-Request-Headers: X-Custom-Header
5 Host: api.alice.com
6 Accept-Language: en-US
7 Connection: keep-alive
8 User-Agent: Mozilla/5.0...

  "预检"请求用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

  该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT

(2)Access-Control-Request-Headers

  该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header

2.2.2 预检请求的回应

  服务器收到"预检"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

 1 HTTP/1.1 200 OK
 2 Date: Mon, 01 Dec 2008 01:15:39 GMT
 3 Server: Apache/2.0.61 (Unix)
 4 Access-Control-Allow-Origin: http://api.bob.com
 5 Access-Control-Allow-Methods: GET, POST, PUT
 6 Access-Control-Allow-Headers: X-Custom-Header
 7 Content-Type: text/html; charset=utf-8
 8 Content-Encoding: gzip
 9 Content-Length: 0
10 Keep-Alive: timeout=2, max=100
11 Connection: Keep-Alive
12 Content-Type: text/plain

  上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

1 Access-Control-Allow-Origin: *

  如果浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息。

1 XMLHttpRequest cannot load http://api.alice.com.
2 Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.

  服务器回应的其他CORS相关字段如下。

1 Access-Control-Allow-Methods: GET, POST, PUT
2 Access-Control-Allow-Headers: X-Custom-Header
3 Access-Control-Allow-Credentials: true
4 Access-Control-Max-Age: 1728000

(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天),在此期间,不用发出另一条预检请求。

2.2.3 浏览器的正常请求和回应

  一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。下面是"预检"请求之后,浏览器的正常CORS请求。

1 PUT /cors HTTP/1.1
2 Origin: http://api.bob.com
3 Host: api.alice.com
4 X-Custom-Header: value
5 Accept-Language: en-US
6 Connection: keep-alive
7 User-Agent: Mozilla/5.0...

  上面头信息的Origin字段是浏览器自动添加的。下面是服务器正常的回应。

1 Access-Control-Allow-Origin: http://api.bob.com
2 Content-Type: text/html; charset=utf-8

  上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

  CORS实现起来比较简单,但是缺点是支持浏览器有限。

     

3、反向代理

  想一下,如果我们请求的时候还是用前端的域名,然后有个东西帮我们把这个请求转发到真正的后端域名上,不就避免跨域了吗?这时候,Nginx出场了。
Nginx配置

 1 server{
 2     # 监听9099端口
 3     listen 9099;
 4     # 域名是localhost
 5     server_name localhost;
 6     #凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
 7     location ^~ /api {
 8         proxy_pass http://localhost:9871;
 9     }    
10 }

  前端就不用干什么事情了,除了写接口,也没后端什么事情了

 1 // 请求的时候直接用回前端这边的域名http://localhost:9099,这就不会跨域,然后Nginx监听到凡是localhost:9099/api这个样子的,都转发到真正的服务端地址http://localhost:9871 
 2 fetch('http://localhost:9099/api/iframePost', {
 3   method: 'POST',
 4   headers: {
 5     'Accept': 'application/json',
 6     'Content-Type': 'application/json'
 7   },
 8   body: JSON.stringify({
 9     msg: 'helloIframePost'
10   })
11 })

  Nginx转发的方式似乎很方便!但这种使用也是看场景的,如果后端接口是一个公共的API,比如一些公共服务获取天气什么的,前端调用的时候总不能让运维去配置一下Nginx,如果兼容性没问题(IE 10或者以上),CROS才是更通用的做法吧。

posted @ 2019-02-09 17:55  北海之北  阅读(1752)  评论(0编辑  收藏  举报