同源策略限制 跨域指的是当两个url的协议,域名,端口三者有一个不同,就会发生跨域。之所以会有跨域,是因为浏览器的同源策略限制。 同源策略限制了一个源的脚本与另一个不同源的脚本之间的交互,这是一种安全保障机制。 是否同源的判断,只要看两个源的url端口和域名是否相同,只要有一个不同,那就是不同源。
跨域的解决方法 jsonp
jsonp简单来说就是通过在html中插入一个script标签,利用其src属性来发起请求,通过在src中为callback添加对应的回调函数来达到对响应数据进行处理的目的
如下例子 发起请求的客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <script > let sendJSONP = function (url, callbackName ) { let script = document .createElement("script" ); script.src = `${url} &callback=${callbackName} ` ; document .head.appendChild(script); }; let getResponse = function (data ) { console .log(`the response you get is "${data} "` ); }; sendJSONP('url/?name=Mike &age=1' , 'getResponse' ); </script > </body > </html >
服务端 node
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const util = require ('util' ), http = require ('http' ), url = require ('url' );let data = JSON .stringify({ message : "I HAVE ALREADY RECEIVED" }); http.createServer(function (req, res ) { req = url.parse(req.url, true ); if (!req.query.callback) res.end(); console .log(`name is ${req.query.name} and his age is ${req.query.age} ` ); res.writeHead(200 , { 'Content-Type' : 'appliction/javascript' }) res.end(req.query.callback + "('" + data + "')" ) }).listen(3000 )
这里打开服务器,然后打开我们写的页面 可以看到服务端控制台已经打印出对应的内容 客户端也打印出相应的响应信息
通过jsonp我们可以跨域发送请求,而且这种方式兼容老的浏览器,但是要注意的是,jsonp只允许发送get请求,而且因为我们是将script标签插入html中,所以无法做到异步请求
CORS
CORS是一个w3c标准,全称是“跨域资源共享”,通过额外的HTTP头来告诉浏览器,让运行在一个origin上的Web应用被准许访问不同域的服务器上指定的资源。 在我们常见的请求中,有很多都是跨域访问的,如img的src属性经常访问不同域的资源,但是出于安全性的考虑,脚本发出的请求,例如XMLHttpRequest和Fetch API遵循同源策略,不能访问不同域的资源。 如果要在脚本中也能访问不同域的资源,可以在服务器中设置CORS接口,这里主要是要服务器和浏览器(兼容性)的支持,前端的请求不需要做出改变。
CORS分简单请求和非简单请求
简单请求 符合简单请求的条件 请求方式为下面三个其中一个
content-type的值仅限下面三个其中之一
text/plain
multipart/form-data
application/x-www-form-urlencoded
对于简单请求,浏览器直接发出CORS请求,也即是说,会自动在请求中添加origin字段。 当服务器接收到请求时,发现发出Origin指定的源不在允许访问的列表内,在响应时就会返回一个没有Access-Control-Allow-Origin 字段的正常HTTP响应。而浏览器发现没有Access-Control-Allow-Origin 字段后,就会知道请求出错,会在控制台中提示。要注意的是,这种错误的状态码是200,所以无法通过状态码来判断是否正确。 可以看到,即使报错,状态码仍是200。
我们可以通过设置Access-Control-Allow-Origin 来让请求得以跨域
举个使用node设置CORS白名单的例子 3000端口的服务端文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 const http = require ('http' ), url = require ('url' );let data = JSON .stringify({ message : "I HAVE ALREADY RECEIVED" }); http.createServer(function (req, res ) { res.setHeader('Access-Control-Allow-Origin' , 'http://localhost:3001' ); req = url.parse(req.url, true ); if (!req.query.callback) res.end(); console .log(`name is ${req.query.name} and his age is ${req.query.age} ` ); res.end(req.query.callback + "('" + data + "')" ) }).listen(3000 )
3001端口的服务端文件
1 2 3 4 5 6 7 8 9 10 11 const http = require ('http' );const fs = require ('fs' ); http.createServer((req, res ) => { const html = fs.readFileSync('test.html' , 'utf-8' ); res.writeHead(200 , { 'Content-Type' : 'text/html' }); res.end(html); }).listen(3001 );
发起请求的页面
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <script > const xhr = new XMLHttpRequest(); xhr.open('get' , 'http://localhost:3000/?name=Mike &age=1' ) xhr.send() </script > </body > </html >
可以看到,这里已经有Access-Control-Allow-Origin 字段了,而且也不报错了。
如果将
1 res.setHeader('Access-Control-Allow-Origin' , 'http://localhost:3001' );
改为
1 res.setHeader('Access-Control-Allow-Origin' , '*' );
我们甚至可以在本地(’file://‘)获取服务端的资源
非简单请求 上面已经提及了简单请求是什么了,其他的请求为非简单请求。 对于非简单请求的CORS请求,会在正式请求前发送一个预请求,询问服务器发起请求的页面所在的域名是否在可访问服务器资源的名单中,如果在的话,正式发起请求,否则报错。
预请求的method为options,我们试着将上面的get请求改为put请求
1 xhr.open('put' , 'http://localhost:3000/?name=Mike &age=1' )
看看控制台 可以看到,这里的method已经变成了OPTIONS了。
同样地,和上面配置允许访问的域名一样,这里我们可以通过设置put请求可访问资源来实现跨域,在3000端口的服务端文件加上一行代码设置
1 2 3 4 5 6 7 8 9 10 11 http.createServer(function (req, res ) { res.setHeader('Access-Control-Allow-Origin' , '*' ); res.setHeader('Access-Control-Allow-Methods' ,'PUT' ) req = url.parse(req.url, true ); if (!req.query.callback) res.end(); console .log(`name is ${req.query.name} and his age is ${req.query.age} ` ); res.end(req.query.callback + "('" + data + "')" ) }).listen(3000 )
重启服务器,再次刷新页面,可以发现不报错了,查看控制台network,发现发起了两个请求 而且我们可以看到,两个请求虽然名字一样,但是Request Method是不一样的,显然一个是预请求,另一个才是真正的请求。
此外,一旦第一次预请求成功确定该请求可访问服务器资源,后面的请求就不必再发起预请求了 修改代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 <!DOCTYPE html > <html lang ="en" > <head > <meta charset ="UTF-8" > <meta name ="viewport" content ="width=device-width, initial-scale=1.0" > <meta http-equiv ="X-UA-Compatible" content ="ie=edge" > <title > Document</title > </head > <body > <button onClick ="send()" > button</button > <script > function send () { const xhr = new XMLHttpRequest(); xhr.open('put' , 'http://localhost:3000/?name=Mike &age=1' ) xhr.send() } </script > </body > </html >
除了第一次点击时会发起两个请求,后面点击都只会发起一个请求。 (这样说其实不太准确,应该是在预请求得到响应后,每次发起请求都不再发起预请求,如果这里快速点击按钮,可能会发起多个预请求)
document.domain
document.domain的处理方式,感觉就像是域名的缩减www.first.hello.com www.second.hello.com
这里两个网址的域名一个为first.hello.com,一个为second.hello.com
可以通过将first.hello.com和second.hello.com的document.domain设置为hello.com就可以做到发送请求时不跨域
window.postMessage()
通过postMessage,我们可以向目标窗口发送MessageEvent消息 MessageEvent四个主要属性
message:类型
data:发送的内容
origin:调用postMessaged的窗口地址
source:调用postMessage的窗口信息
postMessage可以在一个窗口的引用对象上使用,比如当我们在窗口A使用window.open打开一个窗口B后,在打开的窗口B就可以使用window.opener来调用postMessage,这样就可以在打开的窗口B中,将信息发送给不同域的窗口A
举个例子 在CSDN首页使用window.open打开百度页面
1 window .open("https://www.baidu.com" )
然后在CSDN页面监听其他页面发送的信息,打印出监听的信息(e.data)
1 2 3 window .addEventListener('message' ,function (e ) { console .log(e.data); },false )
在百度页面中,向CSDN首页发送信息
1 2 3 window .opener.postMessage({ data :123 },"https://www.csdn.net/" );
在CSDN首页的控制台中,可以看到已经有东西打印出来了
除了使用window.open打开的页面来实现之外,也可以通过iframe的contentWindow属性来实现父窗口向不同域的子窗口发送信息的目的。 假设页面中有这个iframe标签,src的指向和当前页面的地址不同域
1 2 3 <body > <iframe src ="https://www.baidu.com" id ="iframe" frameborder ="0" > </iframe > </body >
在iframe窗口中设置监听
1 2 3 window .addEventListener('message' ,function (e ) { console .log(e.data); },false )
回到top发送信息
1 document .getElementById('iframe' ).contentWindow.postMessage({data :123 },'https://www.baidu.com' )
这里打印出的{data:123}
是在子页面中打印出来的
可以通过设置控制台选项来让控制台只显示一个窗口的内容 可以看到,的确是在子页面打印出的数据,子页面的确收到了不同域的父窗口的信息