【深入】万字长文跨域是怎么回事儿,恐深勿入_字段

跨域的原因

跨域是是因为浏览器的同源策略限制,是浏览器的一种安全机制,服务端之间是不存在跨域的。

所谓同源指的是两个页面具有相同的协议、主机和端口,三者有任一不相同即会产生跨域。

什么是同源策略?

同源策略是一种约定,由Netscape公司1995年引入浏览器,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指"协议+域名+端口"三者相同,即便两个不同的域名指向同一个ip地址,也非同源。

URL组成:

同源策略限制以下几种行为:

  • Cookie、LocalStorage 和 IndexDB 无法读取
  • DOM和JS对象无法获得
  • AJAX 请求不能发送


同源策略在防什么

跨域只存在于浏览器端。而浏览器为 web 提供访问入口。我们在可以浏览器内打开很多页面。正是这样的开放形态,所以我们需要对他有所限制。就比如林子大了,什么鸟都有,我们需要有一个统一的规范来进行约定才能保障这个安全性。

限制不同源的请求,防止JavaScript代码对非同源页面的各种请求(CSRF攻击)

例如用户登录 a 网站,同时新开 tab 打开了 b 网站,如果不限制同源, b 可以像 a 网站发起任何请求,会让不法分子有机可趁。

限制 dom 操作,对其他页面DOM元素(通常包含敏感信息,比如input标签)的读取()

钓鱼网站

跨域举例

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_02

关于ajax跨域请求?

Ajax为什么不能跨域?到底是卡在哪个环节了?。(请求成功了,但客户端浏览器拿不到请求结果)

Ajax其实就是向服务器发送一个GET或POST请求,然后取得服务器响应结果,返回客户端。


理论上这是没有任何问题的,然而普通ajax跨域请求,在服务器端不会有任何问题,只是服务端响应数据返回给浏览器的时候,


浏览器根据响应头的Access-Control-Allow-Origin字段的值来判断是否有权限获取数据,


一般情况下,服务器端如果没有在这个字段做特殊处理的话,跨域是没有权限访问的,所以响应数据被浏览器给拦截了,


所以在ajax回调函数里是获取不到数据的。所以现在ajax跨域的问题可以转化为数据怎么拿回客户端的问题。

 html的script标签,img标签,iframe标签,可以请求第三方的资源(不受同源策略影响)

web页面可以加载放在任意站点的js、css、图片等资源,不会受到"跨域"的影响。


这个时候,我们会想到:既然我们可以调用第三方站点的js,那么如果我们将数据放到第三方站点的js中不就可以将数据带到客户端了吗?


解决方案

1.JSONP跨域

什么是JSONP?

JSONP(JSON with Padding(填充))是JSON的一种“使用模式”,可用于解决主流浏览器的跨域数据访问的问题。


其核心思想是利用JS标签里面的跨域特性进行跨域数据访问,
在JS标签里面存在的是一个跨域的URL,实际执行的时候通过这个URL获得一段字符串,
这段返回的字符串必须是一个合法的JS调用,通过EVAL这个字符串来完成对获得的数据的处理。


即:<script src='url'></script>


JSONP是一个非官方的协议,它允许在服务器端集成Script tags返回至客户端,
通过javascript callback的形式实现跨域访问(这仅仅是JSONP简单的实现形式)。

JSONP的粗糙实现

下面我们通过一个例子来说明一下JSONP是如何实现ajax跨域请求的。

html 代码

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>jsonp</div>
</body>
<script>
function getremotedata(data) {
console.log(data);
}
var div = document.getElementsByTagName('div');


div[0].onclick = function(){
var url = "./getdata.js";
var script = document.createElement('script');
script.setAttribute('src', url);
document.getElementsByTagName('head')[0].appendChild(script);
};
</script>
</html>

远程的getdata.js

getremotedata({
code:0,
result:'success'
});

得到的结果:

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_03

下图是 jsonp请求的流程图

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_04

josnp 优缺点分析:

优点:

1.1它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制,JSONP可以跨越同源策略;

1.2它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持;

1.3在请求完毕后可以通过调用callback的方式回传结果。
将回调方法的权限给了调用方。这个就相当于将controller层和view层终于分开了。
我提供的jsonp服务只提供纯服务的数据,至于提供服务以 后的页面渲染和后续view操作都由调用者来自己定义就好了。
如果有两个页面需要渲染同一份数据,你们只需要有不同的渲染逻辑就可以了,
逻辑都可以使用同 一个jsonp服务。

缺点

2.1它只支持GET请求而不支持POST等其它类型的HTTP请求


2.2它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题。


2.3 jsonp在调用失败的时候不会返回各种HTTP状态码。


2.4缺点是安全性。万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的。
那么结果是什么?所有调用这个 jsonp的网站都会存在漏洞。
于是无法把危险控制在一个域名下…所以在使用jsonp的时候必须要保证使用的jsonp服务必须是安全可信的。

2. 跨域资源共享(CORS)

CORS是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。

它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。

整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

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

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


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


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

这是为了兼容表单(form),因为历史上表单一直可以发出跨域请求。AJAX 的跨域设计就是,只要表单可以发,AJAX 就可以直接发。

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

简单请求

对于简单请求,浏览器直接发出CORS请求。具体来说,就是在头信息之中,增加一个Origin字段。

下面是一个例子,浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段。


GET /cors HTTP/1.1 

Origin: http://api.bob.com 

Host: api.alice.com 

Accept-Language: en-US 

Connection: keep-alive 

User-Agent: Mozilla/5.0...


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

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

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


Access-Control-Allow-Origin: http://api.bob.com 

Access-Control-Allow-Credentials: true 

Access-Control-Expose-Headers: FooBar 

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-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

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

Access-Control-Allow-Credentials: true

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

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

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。

但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials。

xhr.withCredentials = false;

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

非简单请求

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

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

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


var url = 'http://api.alice.com/cors'; 

var xhr = new XMLHttpRequest(); 

xhr.open('PUT', url, true); 

xhr.setRequestHeader('X-Custom-Header', 'value'); 

xhr.send();


上面代码中,HTTP请求的方法是PUT,并且发送一个自定义头信息X-Custom-Header。

浏览器发现,这是一个非简单请求,就自动发出一个"预检"请求,要求服务器确认可以这样请求。下面是这个"预检"请求的HTTP头信息。


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...


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

除了Origin字段,"预检"请求的头信息包括两个特殊字段。

(1)Access-Control-Request-Method

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

(2)Access-Control-Request-Headers

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

服务器收到"预检"请求以后,检查了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回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。

Access-Control-Allow-Origin: *

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

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

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

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true
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天),在此期间,不用发出另一条预检请求。

一旦服务器通过了"预检"请求,以后每次浏览器正常的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字段是每次回应都必定包含的。

与JSONP的比较

CORS与JSONP的使用目的相同,但是比JSONP更强大。

JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

3.nginx代理跨域

分析前准备:

前端网站地址:http://localhost:8080

服务端网址:http://localhost:59200 

首先保证服务端是没有处理跨域的,其次,先用postman测试服务端接口是正常的

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_05

当网站8080去访问服务端接口时,就产生了跨域问题,那么如何解决?接下来我把跨域遇到的各种情况都列举出来并通过nginx代理的方式解决(后台也是一样的,只要你理解的原理)。

跨域主要涉及4个响应头:

  • Access-Control-Allow-Origin 用于设置允许跨域请求源地址 (预检请求和正式请求在跨域时候都会验证)
  • Access-Control-Allow-Headers 跨域允许携带的特殊头信息字段 (只在预检请求验证)
  • Access-Control-Allow-Methods 跨域允许的请求方法或者说HTTP动词 (只在预检请求验证)
  • Access-Control-Allow-Credentials 是否允许跨域使用cookies,如果要跨域使用cookies,可以添加上此请求响应头,值设为true(设置或者不设置,都不会影响请求发送,只会影响在跨域时候是否要携带cookies,但是如果设置,预检请求和正式请求都需要设置)。不过不建议跨域使用(项目中用到过,不过不稳定,有些浏览器带不过去),除非必要,因为有很多方案可以代替。

网上很多文章都是告诉你直接Nginx添加这几个响应头信息就能解决跨域,当然大部分情况是能解决,但是我相信还是有很多情况,明明配置上了,也同样会报跨域问题。

什么是预检请求?:当发生跨域条件时候,览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。如下图

【深入】万字长文跨域是怎么回事儿,恐深勿入_字段_06

开始动手模拟:

Nginx代理端口:22222 ,配置如下

server {
listen 22222;
server_name localhost;
location / {
proxy_pass http://localhost:59200;
}
}

测试代理是否成功,通过Nginx代理端口2222再次访问接口,可以看到如下图通过代理后接口也是能正常访问

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_07

接下来开始用网站8080访问Nginx代理后的接口地址,报错情况如下↓↓↓

情况1: 

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_08

通过错误信息可以很清晰的定位到错误(注意看标红部分)priflight说明是个预请求,CORS 机制跨域会首先进行 preflight(一个 OPTIONS 请求), 该请求成功后才会发送真正的请求。这一设计旨在确保服务器对 CORS 标准知情,以保护不支持 CORS 的旧服务器

通过错误信息,我们可以得到是预检请求的请求响应头缺少了 Access-Control-Allow-Origin,错哪里,我们改哪里就好了。修改Nginx配置信息如下(红色部分为添加部分),缺什么就补什么,很简单明了

server {
listen 22222;
server_name localhost;
location / {
add_header Access-Control-Allow-Origin 'http://localhost:8080';
proxy_pass http://localhost:59200;
}
}

当满怀欢喜的以为能解决后,发现还是报了同样的问题

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_08

不过我们的配置没什么问题,问题在Nginx,下图链接http://nginx.org/en/docs/http/ngx_http_headers_module.html

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_10

add_header 指令用于添加返回头字段,当且仅当状态码为图中列出的那些时有效。如果想要每次响应信息都携带头字段信息,需要在最后添加always(经我测试,只有Access-Control-Allow-Origin这个头信息需要加always,其他的不加always也会携带回来),那我们加上试试

server {
listen 22222;
server_name localhost;
location / {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
proxy_pass http://localhost:59200;
}
}

修改了配置后,发现生效了,当然不是跨域就解决了,是上面这个问题已经解决了,因为报错内容已经变了

情况2:

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: It does not have HTTP ok status.

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_11

通过报错信息提示可以得知,是跨域浏览器默认行为的预请求(option请求)没有收到ok状态码,此时再修改配置文件,当请求为option请求时候,给浏览器返回一个状态码(一般是204)

server {
listen 22222;
server_name localhost;
location / {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://localhost:59200;
}
}

当配置完后,发现报错信息变了

情况3:

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: Request header field authorization is not allowed by Access-Control-Allow-Headers in preflight response.

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_12

意思就是预请求响应头Access-Control-Allow-Headers中缺少头信息authorization(各种情况会不一样,在发生跨域后,在自定义添加的头信息是不允许的,需要添加到请求响应头Access-Control-Allow-Headers中,以便浏览器知道此头信息的携带是服务器承认合法的,我这里携带的是authorization,其他的可能是token之类的,缺什么加什么),知道了问题所在,然后修改配置文件,添加对应缺少的部分,再试试

server {
listen 22222;
server_name localhost;
location / {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
if ($request_method = 'OPTIONS') { add_header Access-Control-Allow-Headers 'authorization'; #为什么写在if里面而不是接着Access-Control-Allow-Origin往下写?因为这里只有预检请求才会检查 return 204; } proxy_pass http://localhost:59200; }
}

此时发现报错问题又回到了情况1

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_13

经测试验证,只要if ($request_method = 'OPTIONS') 里面写了 add_header ,当为预检请求时外部配置的都会失效,为什么?↓↓。

官方文档是这样说的:

There could be several add_header directives. These directives are inherited from the previous level if and only if there are no add_header directives defined on the current level.

意思就是当前层级无 add_header 指令时,则继承上一层级的add_header。相反的若当前层级有了add_header,就应该无法继承上一层的add_header。

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_14

配置修改如下:

server {
listen 22222;
server_name localhost;
location / {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin 'http://localhost:8080';
add_header Access-Control-Allow-Headers 'authorization';
return 204;
}
proxy_pass http://localhost:59200;
}
}

此时改完发现跨域问题已经解决了,

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_15

不过以上虽然解决了跨域问题,但是考虑后期可能Nginx版本更新,不知道这个规则会不会被修改,考虑到这样的写法可能会携带上两个 Access-Control-Allow-Origin ,这种情况也是不允许的,下面会说到。所以配置适当修改如下:

server {
listen 22222;
server_name localhost;
location / {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin 'http://localhost:8080';
add_header Access-Control-Allow-Headers 'authorization';
return 204;
}
if ($request_method != 'OPTIONS') {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
}
proxy_pass http://localhost:59200;
}
}

还没完,继续聊 ↓↓

情况4:

比较早期的API可能只用到了POST和GET请求,而Access-Control-Allow-Methods这个请求响应头跨域默认只支持POST和GET,当出现其他请求类型时候,同样会出现跨域异常。

比如,我这里将请求的API接口请求方式从原来的GET改成PUT,在发起一次试试。在控制台上会抛出错误:

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy:

Method PUT is not allowed by Access-Control-Allow-Methods in preflight response.

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_16

报错内容也讲的很清楚,在这个预请求中,PUT方法是不允许在跨域中使用的,我们需要改下Access-Control-Allow-Methods的配置(缺什么加上么,这里我只加了PUT,可以自己加全一点),让浏览器知道服务端是允许的

server {
listen 22222;
server_name localhost;
location / {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin 'http://localhost:8080';
add_header Access-Control-Allow-Headers 'content-type,authorization';
add_header Access-Control-Allow-Methods 'PUT';#为这么只加在这个if中,不再下面的if也加上?因为这里只有预检请求会校验,当然你加上也没事。
return 204;
}
if ($request_method != 'OPTIONS') {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
}
proxy_pass http://localhost:59200;
}
}

这里注意一下,改成PUT类型后,Access-Control-Allow-Headers请求响应头又会自动校验content-type这个请求头,和情况3是一样的,缺啥补啥就行了。如果不加上content-type,则会报如下错误。(想简单的话,Access-Control-Allow-Headers和Access-Control-Allow-Methods可以设置为 * ,表示全都匹配。但是Access-Control-Allow-Origin就不建议设置成 * 了,为了安全考虑,限制域名是很有必要的。)

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_17

都加上后,问题就解决了,这里报405是我服务端这个接口只开放了GET,没有开放PUT,而此刻我将此接口用PUT方法去请求,所以接口会返回这个状态码。

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_18

情况5:

最后再说一种情况,就是后端处理了跨域,就不需要自己在处理了(这里吐槽下,某些后端工程师自己改服务端代码解决跨域,但是又不理解其中原理,网上随便找段代码黏贴,导致响应信息可能处理不完全,如method没添加全,headers没加到点上,自己用的那个可能复制过来的并不包含实际项目所用到的,没有添加options请求返回状态码等,导致Nginx再用通用的配置就会可能报以下异常)

Access to XMLHttpRequest at 'http://localhost:22222/api/Login/TestGet' from origin 'http://localhost:8080' has been blocked by CORS policy: The 'Access-Control-Allow-Origin' header contains multiple values '*, http://localhost:8080', but only one is allowed.

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_19

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_20

意思就是此刻Access-Control-Allow-Origin请求响应头返回了多个,而只允许有一个,这种情况当然修改配置去掉Access-Control-Allow-Origin这个配置就可以了,不过遇到这种情况,建议Nginx配置和服务端自己解决跨域只选其一。(这里注意如果按我上面的写法,if $request_method = 'OPTIONS' 这个里面的Access-Control-Allow-Origin可不能删除,删除!='OPTIONS'里面的就好了,因为这里如果是预检请求直接就ruturn了,请求不会再转发到59200服务,如果也删除了,就会报和情况1一样的错误。所以为什么说要不服务端代码层面解决跨域,要不就Nginx代理解决,不要混着搞,不然不明白原理的人,网上找一段代码贴就很可能解决不了问题)

 ↓ ↓ ↓ ↓ ↓

再贴一份完整配置(*号根据自己‘喜好’填写):

server {
listen 22222;
server_name localhost;
location / {
if ($request_method = 'OPTIONS') {
add_header Access-Control-Allow-Origin 'http://localhost:8080';
add_header Access-Control-Allow-Headers '*';
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Credentials 'true';
return 204;
}
if ($request_method != 'OPTIONS') {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
add_header Access-Control-Allow-Credentials 'true';
}
proxy_pass http://localhost:59200;
}
}

或者:

server {
listen 22222;
server_name localhost;
location / {
add_header Access-Control-Allow-Origin 'http://localhost:8080' always;
add_header Access-Control-Allow-Headers '*';
add_header Access-Control-Allow-Methods '*';
add_header Access-Control-Allow-Credentials 'true';
if ($request_method = 'OPTIONS') {
return 204;
}
proxy_pass http://localhost:59200;
}
}

4.  proxy服务器

在知道怎么跨域之前,先知道这个代理是webpack提供给我们的,常说的代理就是一个代理服务器,例如A服务器请求B服务器,我们可以通过代理C服务器去帮助我们请求 , 产生的跨域原因就是浏览器的同源政策是针对于ajax的请求,并不限制服务器之间的通信传输,而这个代理服务器正是和我相同端口域名的,我只需去用代理服务器去发请求再去接收,从而达到跨域。

【深入】万字长文跨域是怎么回事儿,恐深勿入_字段_21

举个生活中的栗子

例如我们都会去从某宝、东去买东西,但是呢商家不会去亲手把商品送给你,原因呢就是太远 可以理解为跨域啦, 商家就会去从利用快递的形式送到你家,而这个快递被送邮的过程就相当于代理服务器, 下面的图优点抽象,但是接近于生活。

【深入】万字长文跨域是怎么回事儿,恐深勿入_跨域_22


提前注意点 proxy只限于开发状态下使用

webpack-dev-server

//vue.config.js
devServer: {
proxy: {
// 如果请求地址以/api打头,就出触发代理机制
// http://localhost:8080/api/login -> http://localhost:3000/api/login
'/api': {
target: 'http://localhost:3000' // 我们要代理的真实接口地址
}
}
},

/api/ :代表请求路径以api开头的就将代理请求到 http://localhost:3000

target:代表代理到的目标地址

proxy工作原理实质上是利用http-proxy-middleware 这个http代理中间件,实现请求转发给其他服务器,对于为什么只在开发服务器,因为技术只是在webpack打包阶段临时生成了node serve,来实现nginx的proxy_pass的反向代理

总结

  • 代理服务和前端服务之间由于协议域名端口三者统一不存在跨域问题,可以直接发送请求
  • 代理服务和后端服务之间由于并不经过浏览器没有同源策略的限制,可以直接发送请求
  • 修改配置文件后 重启项目
  • ajax的基地址baseUrl必须是相对地址,而不能是绝对地址


5. document.domain + iframe跨域

document.domain用来得到当前网页的域名。

比如在百度(https://www.baidu.com)页面控制台中输入:

alert(document.domain);              //"www.baidu.com"

我们也可以给document.domain属性赋值,不过是有限制的,你只能赋成当前的域名或者一级域名。
比如:

alert(document.domain = "baidu.com");     //"baidu.com"
alert(document.domain = "www.baidu.com"); //"www.baidu.com"

上面的赋值都是成功的,因为www.baidu.com是当前的域名,而baidu.com是一级域名。

但是下面的赋值就会出来"参数无效"的错误:
比如:

alert(document.domain = "qq.com");     //参数无效  报错
alert(document.domain = "www.qq.com"); //参数无效 报错

因为qq.com与baidu.com的一级域名不相同,所以会有错误出现。
这是为了防止有人恶意修改document.domain来实现跨域偷取数据。

利用document.domain 实现跨域

前提条件:

这两个域名必须属于同一个一级域名!而且所用的协议,端口都要一致,否则无法利用document.domain进行跨域。

Javascript出于对安全性的考虑,而禁止两个或者多个不同域的页面进行互相操作。

而相同域的页面在相互操作的时候不会有任何问题。

1.比如:

baidu.com的一个网页(baidu.html)里面 利用iframe引入了一个qq.com里的一个网页(qq.html)。

这时在baidu.html里面可以看到qq.html里的内容,但是却不能利用javascript来操作它。因为这两个页面属于不同的域,在操作之前,js会检测两个页面的域是否相等,如果相等,就允许其操作,如果不相等,就会拒绝操作。

这里不可能把baidu.html与qq.html利用JS改成同一个域的。因为它们的一级域名不相等。(强制用JS将它们改成相等的域的话会报跟上面一样的"参数无效错误。")

但如果在baidu.html里引入baidu.com里的另一个网页,是不会有这个问题的,因为域相等。

2.另一种情况,有两个子域名:

news.baidu.com(news.html)

map.baidu.com(map.html)

news.baidu.com里的一个网页(news.html)引入了map.baidu.com里的一个网页(map.html)

这时news.html里同样是不能操作map.html里面的内容的。

因为document.domain不一样,一个是news.baidu.com,另一个是map.baidu.com。

这时我们就可以通过Javascript,将两个页面的domain改成一样的,

需要在a.html里与b.html里都加入:

document.domain = “baidu.com”;

这样这两个页面就可以互相操作了。也就是实现了同一一级域名之间的"跨域"。

news.baidu.com下的news.html页面:

<script>
document.domain = 'baidu.com';
var ifr = document.createElement('iframe');
ifr.src = 'map.baidu.com/map.html';
ifr.style.display = 'none';
document.body.appendChild(ifr);
ifr.onload = function(){
var doc = ifr.contentDocument || ifr.contentWindow.document;
// 这里可以操作map.baidu.com下的map.html页面
var oUl = doc.getElementById('ul1');
alert(oUl.innerHTML);
ifr.onload = null;
};
</script>

map.baidu.com下的map.html页面:

<ul id="ul1">我是map.baidu.com中的ul</ul>
<script>
document.domain = 'baidu.com';
</script>

实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。

6.location.hash + iframe跨域

利用location.hash和iframe可以解决完全跨域的问题。其原理是利用location.hash传值,创建定时器,坚持hash的变化,执行相应的操作。
下面我们来完成一个案例:
在bao.com域名下有个index.html, 在index.html中通过iframe引入hui.com域名下header.html。使index.html和header.html可以相互通信。

index.html代码:

<html>
<meta charset="UTF-8" />
<h1>通过IFRAME 和 location.hash 进行javasript 跨域</h1>


引入header.html
<iframe id="iframe" src="http://www.hui.com/header.html"></iframe>
<input type="button" onClick="changecolor()" value="change color" id="btn" />
<script>
var iframe = document.getElementById('iframe');
var _timeid = setInterval(checkHash, 1000);function checkHash() {
switch (location.hash) {
case '#mainred':
document.getElementById("btn").style.color = "red";
break;
}
}


function changecolor() {
iframe.src = 'http://www.hui.com/huanhuba/modelWebsite/js/iframe/hash/header.html#red';
}
</script>
</html>

hui.com下的 header.html代码:

<html>
<meta charset="UTF-8" />
<body>
<a href="javascript:;" id="btn">点击改变主页面按钮颜色</a>
<iframe src="http://www.bao.com/huanhuba/modelWebsite/js/iframe/hash/crossdomain.html" width="" height="" id="cdiframe" style="display: none;"></iframe>
<script>
var _timeid = setInterval(checkHash, 1000);
function checkHash() {
switch (location.hash) {
case '#red':
callback();
break;
}
}
function callback() {
document.getElementById("btn").style.color = "red";
}


document.getElementById("btn").onclick = function(){
document.getElementById("cdiframe").src = document.getElementById("cdiframe").src + "#mainred";
};
</script>
</body>
</html>

现阶段index.html中可以更改引入的iframe的src的hash。header.html中通过定时器判断hash值的变化,做出相应的操作。但是在header.html不可以直接操作index.html的hash。如果直接用parent.locatin.hash = name;会出现
111111113324禁止跨域操作
此时需要引入一个代理文件(crossdomain.html),此文件与index.html同域,因此,index.html可以和crossdomian.html相互通信。通过在header.html中改变crossdomain.html的hash,在crossdomain.html中监听
hash的变化,在通过parent.parent.location.hash改变index.html的hash值。

crossdomain.html代码:

<html>
<meta charset="UTF-8" />
<script>


/****crossmain 和 index在同一域名下可以相互通信**************/


var _timeid = setInterval(checkHash, 1000);
function checkHash(){
parent.parent.location.hash = self.location.hash.substring(1);
}
</script>
</html>

location.hash + iframe跨域的优点:
1.可以解决域名完全不同的跨域
2.可以实现双向通讯

location.hash + iframe跨域的缺点:
location.hash会直接暴露在URL里,并且在一些浏览器里会产生历史记录,数据安全性不高也影响用户体验。另外由于URL大小的限制,支持传递的数据量也不大。

实现原理:a欲与b跨域相互通信,通过中间页c来实现。三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。

7.window.name + iframe跨域

window.name属性有这样的特点:
当前页设置的值, 在页面重新加载(非同域也可以)后, 值依然不变.
比如:

window.name = 'abc';
window.name; // abc
window.location = 'http://www.baidu.com';
window.name; // abc

利用这个加上iframe就可以实现跨域数据传递.
iframe会创建一个 新的窗口(也就是一个 干净的环境), 也有name属性.

下面来实验下:

├── one/
│ ├── a.html
│ └── proxy.html
└── two/
└── b.html

利用http-server来模拟跨域的情况:

cd one/
screen -S one http-server -p 8001
cd ../two/
screen -S two http-server -p 8002
google-chrome http://localhost:8001/a.html

注意上述用了端口不同来制造非同源的情况.
其中页面主要代码如下:
proxy.html是一个空白页面, 主要是为了和a.html通信用.

b.html提供数据用:

<script>
window.name = 'b.html\'s data';
</script>

a.html

<script type="text/javascript">
var otherLoaded = false,
iframe = document.createElement('iframe'),
loadfn = function() {
if (otherLoaded) {
var data = iframe.contentWindow.name; // 读取数据
alert(data); // 弹出b.html's data


// 清理工作
iframe.contentWindow.document.write('');
iframe.contentWindow.close();
document.body.removeChild(iframe);
} else if (!otherLoaded) {
otherLoaded = true;
iframe.contentWindow.location = "http://localhost:8001/proxy.html"; // 设置的代理文件
}
};
iframe.src = 'http://localhost:8002/b.html';
if (iframe.attachEvent) {
iframe.attachEvent('onload', loadfn);
} else {
iframe.onload = loadfn;
}
document.body.appendChild(iframe);
</script>

可以看到, 第一次设置iframe的地址为b.html, 这样的话b.html会被加载进来,

但是并不能直接访问iframe.contentWindow.name, 因为a.html和b.html目前不同源,

如果将loadfn的实现改为var data = iframe.contentWindow.name;,会出来这个错误:

a.html:14 Uncaught DOMException: Blocked a frame with origin "http://localhost:8001" from accessing a cross-origin frame.

那怎么办呢, 既然不同源, 就改成同源呗, 所以将iframe地址改成与a.html同源的proxy.html,

由于window.name在地址变化时值不变, 所以iframe.contentWindow.name的值还是之前的值, 也就是b.html窗口的值, 而又满足的同源的要求, 所以可以访问成功.

8.postMessage跨域

window.postMessage()方法可以安全地实现Window对象之间的跨域通信。例如,在一个页面和它生成的弹出窗口之间,或者是页面和嵌入其中的iframe之间。

通常情况下,不同页面上的脚本允许彼此访问,当且仅当它们源自的页面共享相同的协议,端口号和主机(也称为“同源策略”)。window.postMessage()提供了一个受控的机制来安全地规避这个限制(如果使用得当的话)。

一般来说,一个窗口可以获得对另一个窗口的引用(例如,通过targetWindow=window.opener),然后使用targetWindow.postMessage()在其上派发MessageEvent。接收窗口随后可根据需要自行处理此事件。传递给window.postMessage()的参数通过事件对象暴露给接收窗口。

发送端

postMessage程序

var receiver = document.getElementById('receiver').contentWindow;
var btn = document.getElementById('send');
btn.addEventListener('click', function (e) {
e.preventDefault();
var val = document.getElementById('text').value;
receiver.postMessage("Hello "+val+"!", "http://res.42du.cn");
});

发送消息的基本语法:

targetWindow.postMessage(message, targetOrigin, [transfer]);

targetWindow

targetWindow就是接收消息的窗口的引用。获得该引用的方法包括:

  • Window.open
  • Window.opener
  • HTMLIFrameElement.contentWindow
  • Window.parent
  • Window.frames +索引值

message

message就是要发送到目标窗口的消息。数据使用结构化克隆算法进行序列化。这意味着我们可以将各种各样的数据对象安全地传递到目标窗口,而无需自己对其进行序列化。

targetOrigin

targetOrigin就是指定目标窗口的来源,必须与消息发送目标相一致,可以是字符串“*”或URI。*表示任何目标窗口都可接收,为安全起见,请一定要明确提定接收方的URI。

transfer

transfer是可选参数

接收端

目标窗口通过执行下面的JavaScript来侦听发送过来的消息:

window.addEventListener("message", receiveMessage, false);
function receiveMessage(event){
if (event.origin !== "http://www.42du.cn")
return;
}

event对象有三个属性,分别是origin,data和source。event.data表示接收到的消息;event.origin表示postMessage的发送来源,包括协议,域名和端口;event.source表示发送消息的窗口对象的引用; 我们可以用这个引用来建立两个不同来源的窗口之间的双向通信。

完整程序

发送程序

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>42度空间-window.postMessage()跨域消息传递</title>
</head>
<body>
<div>
<input id="text" type="text" value="42度空间" />
<button id="send" >发送消息</button>
</div>
<iframe id="receiver" src="http://res.42du.cn/static/html/receiver.html" width="500" height="60">
<p>你的浏览器不支持IFrame。</p>
</iframe>
<script>
window.onload = function() {
var receiver = document.getElementById('receiver').contentWindow;
var btn = document.getElementById('send');
btn.addEventListener('click', function (e) {
e.preventDefault();
var val = document.getElementById('text').value;
receiver.postMessage("Hello "+val+"!", "http://res.42du.cn");
});
}
</script>
</body>
</html>

接收程序

<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<title>42度空间-从www.42du.cn接收消息</title>
</head>
<body>
<div id="message">
Hello World!
</div>
<script>
window.onload = function() {
var messageEle = document.getElementById('message');
window.addEventListener('message', function (e) {
alert(e.origin);
if (e.origin !== "http://www.42du.cn") {
return;
}
messageEle.innerHTML = "从"+ e.origin +"收到消息: " + e.data;
});
}
</script>
</body>
</html>

9.WebSocket协议跨域

websocket如何实现跨域通信?

原理:利用webSocket的API,可以直接new一个socket实例,然后通过open方法内send要传输到后台的值,也可以利用message方法接收后台传来的数据。后台是通过new WebSocket.Server({port:3000})实例,利用message接收数据,利用send向客户端发送数据。具体看以下代码:

代码:

本地域打开socket.html

WebSocket是高级api,不兼容,但是可以使用socket.io这个库,这个库做了兼容处理

<!DOCTYPE html>
<html>
<head>
<title></title>
</head>
<body>
<!--
高级api 不兼容 但是有一个socket.io这个库,是兼容的(一般用这个)


-->


<script type="text/javascript">
let socket = new WebSocket("ws://localhost:3000");//ws协议是webSocket自己创造的
socket.onopen = function(){
socket.send("我叫俞华");
}
socket.onmessage = function(e){
console.log(e.data);//你好,我叫俞华!
}
</script>
</body>
</html>
  • 起一个服务端
  • 一般起的服务是http服务,但是websocket需要起ws服务,ws是webSocket自己定义的。
/*
要使用ws协议,那么就要装一个ws的包
*/
let express = require("express");
let app = express();
let WebSocket = require("ws");
let wss = new WebSocket.Server({port:3000});
wss.on("connection",function(ws){//先连接
ws.on("message",function(data){//用message来监听客户端发来的消息
console.log(data);//俞华
ws.send("你好,"+data+"!");
})
})

如何保证websocket的通信会话是唯一的?

  • 建立WebSocket链接的url上加上时间戳。

10.浏览器开启跨域

其实跨域问题是浏览器策略,源头是他,关闭这个功能

Windows

找到你安装的目录

.\Google\Chrome\Application\chrome.exe --disable-web-security --user-data-dir=xxxx

Mac

~/Downloads/chrome-data这个目录可以自定义.

总结

jsonp(只支持get请求,支持老的IE浏览器)适合加载不同域名的js、css,img等静态资源;

CORS(支持所有类型的HTTP请求,但浏览器IE10以下不支持)适合做ajax各种跨域请求;

Nginx代理跨域和nodejs中间件跨域原理都相似,都是搭建一个服务器,直接在服务器端请求HTTP接口,这适合前后端分离的前端项目调后端接口。

document.domain+iframe适合主域名相同,子域名不同的跨域请求。

postMessage、websocket都是HTML5新特性,兼容性不是很好,只适用于主流浏览器和IE10+。

【深入】万字长文跨域是怎么回事儿,恐深勿入_服务器_23


关注公众号 soft张三丰 

【深入】万字长文跨域是怎么回事儿,恐深勿入_字段_24