一,什么是跨域;同源策略又是什么
二,CORS跨域资源共享(官方推荐)
三,JSONP(野路子 有局限 好用)
四,iframe相关跨域(document.domain;window.name;location.hash)
五,HTML5相关跨域(postMessage(); WebSocket)
六,服务器代理跨域(node中间件;nginx反向代理)
七,总结
个人能理解:为了防止除自身域外的其他域,恶意获取或篡改网站的信息和样式,出于浏览器安全的基础上提出了'同源策略';而同源策略的要求就是地址源的'协议 域名 端口'这些全部相同,如有一个不同就是不同源,称之为跨域;以下是域名是否同源的例子
1 2 3 4 5 6 7 |
eg:http://wellwin.top:80;http://是协议,www是子域,wellwin是主域,80是端口(一般都默认不用写) 1.http://wellwin.top //不同源 协议不一样 2.http://ve.wellwin.top //不同源 子域不一样 3.http://www.wellwell.top //不同源 主域不一样 4.http://wellwin.top:81 //不同源 端口不一样 5.http://ve.wellwin.top //不同源 子域不一样 6.http://wellwin.top/a/b.html //同源 协议,域名,端口都一样,不同文件夹也也可以 |
但是很显然我们工作学习中经常会用到非同源下的资源,这种时候就要求我们必须要进行跨域请求 ,因此也催生出了一系列的跨域方案,这里我们逐一介绍
CORS的全称是跨域资源共享(Cross-Origin Resource Sharing) ,是一种Ajax跨域请求的方案。大家都知道Ajax是浏览器提供的一套可以向服务端发送获取数据实现无刷新、动态获取数据的Api;受同源策略的影响它原本是不能跨域的,后来W3C颁布了一项CORS('跨域资源共享')技术,允许浏览器跨域发送XMLHttpRequest请求;现在通过服务端添加白名单,Ajax请求就可以轻松实现跨域;
CORS方案是通过浏览器端发送跨域的XMLHttpRequest请求,再配合服务端配置来进行跨域的(Ajax+服务端允许);它和同源的Ajax请求代码几乎是一样的,所有的动作都是浏览器和服务端处理的,所以更侧重于服务端的配置;在发送跨域请求的时候,浏览器会将CORS请求分为简单请求和非简单请求;非简单请求比简单请求多一次Option '预检'请求;
满足一下两个条件就是简单请求,不满足就是非简单请求
(1).简单请求:浏览器会直接发出一个带有Origin信息头的CORS请求,这个Origin就是本次请求的地址源(协议+域名+端口),然后服务端会检查 'Access-Control-Allow-Origin '是否设置了允许当前源(Origin)访问;
1 2 3 4 5 6 7 8 9 10 11 12 |
//eg:简单的CORS+Ajax get请求 //php服务端 header('Access-Control-Allow-Origin:http://wellwin.top'); //js 文件所在地址是:www.wellwin.top var xhr=new XMLHttpRequest(); xhr.open('GET','http://服务端地址'); xhr.send(); xhr.onreadystatechange=function(){ if (this.readyState==4&this.status==200) { console.log(this.responseText) } } |
如果服务端不允许当前源的访问,会返回一个正常的HTTP回应,浏览器检查回应头信息没有' Access-Control-Allow-Origin '字段,就知道不允许跨域;如果服务器设置了允许当前源访问,会在HTTP回应中添加以下几个信息头 ,浏览器检查有'Access-Control-Allow-Origin: http://wellwin.top'字段,就知道是允许此源跨域的
1 2 3 4 5 6 7 8 9 10 |
//被允许访问的域名;'*'表示允许所有域访问 Access-Control-Allow-Origin: http://wellwin.top //表示是否允许浏览器发送cookie值;需配合Ajax设置xhr.withCredentials=true使用;而且当需要发送cooKie的时候上面那个就不能设为'*',必须要有清楚的域名 Access-Control-Allow-Credentials: true //该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。 Access-Control-Expose-Headers: FooBar Content-Type: text/html; charset=utf-8 |
(2).非简单请求:浏览器会先发一个Option请求到服务端,Option'预检请求'的请求头除了Origin请求源之外,还有两个特殊的字段:Access-Control-Request-Method (什么请求方式);Access-Control-Request-Headers (浏览器CORS请求额外发送的头部信息字段);服务器接收到浏览器预检之后检查Origin以及 Access-Control-Request-Method , Access-Control-Request-Headers ,服务端确认允许(此域的该种请求方式及该种请求头)跨域之后再给出浏览器反馈;以下是浏览器设置请求头为Content-Type: application/json 的非简单的POST请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
//php服务端设置 header('Access-Control-Allow-Origin:http://wellwin.top');//允许请求的地址 header('Access-Control-Allow-Methods:POST,GET,HEAD');//允许请求的方式 header('Access-Control-Allow-Headers:x-requested-with,content-type');//x-requested-with请求头信息表明请求是异步请求 //js 文件所在地址是:www.wellwin.top var xhrPost=new XMLHttpRequest(); xhrPost.open('POST','http://服务端地址'); xhrPost.setRequestHeader("Content-Type",'application/json');//设置请求头 xhrPost.send(); xhrPost.onreadystatechange=function(){ if (this.readyState==4) { console.log(this.responseText); } } |
一旦通过了'预检'请求之后,浏览器每次再发送CORS请求都跟正常的简单请求一样
优点:允许所有的跨域类型请求,可载信息量大,安全可靠,与同源Ajax代码相似配置简单
缺点:浏览器要求现代浏览器或要IE10以上
JSONP是“JSON with padding”的简写,可以将其翻译为“被包裹的JSON” 。是一种简单易用、流传广泛的非官方经典跨域方案。JSON与JSONP不单单只是一个"P"的区别;JSON是一种数据格式,JSONP是一种借用JSON数据格式的跨域请求方案。他们既有联系又大不相同
在这里我们可以模拟一下前辈们是如何发现推导JSONP跨域请求方案的;在使用HTML标签时我们会发现link、img、a、iframe、script等标签的src属性和href属性是不受同源策略影响的,但是我们跨域是要拿到数据并使用的,基于这点考虑发现这些标签中script标签的src是能拿到链接文件的内容并使用的,由此我们发现了JSONP跨域请求的大门
1 2 3 4 |
//a.js 地址:localtest/a.js console.log('我是localtest下的js') //b.html 地址:localhost/b.html <script src='localtest/a.js'></script> |
开心的同时我们也发现了一些问题:1,如果请求的数据不是javaScript规定的数据类型就会报错;2,就算是规定的数据类型,我们还是要将其放入到变量中或者其他方式才可以调用(我们知道一个数据直接暴漏在js中时会报错的);那么怎么规避这两个错误呢?
我们知道JSON是一种支持的多种数据类型的数据类型,而他也刚好被javaScript所支持,那么我们可以将服务端的数据写成JSON的格式传给客户端,这样就不用担心数据类型的问题了;/*把服务端数据写成JSON格式*/
数据类型的问题解决了,我们又如何处理返回回来的数据呢,我们最先想到的是把数据赋值给一个全局变量,但是扔给全局变量的话会导致一些问题(全局暴露、命名冲突、数据处理逻辑分散等等),还可以把数据当作参数丢给函数,这样就不存在这些问题了/*让服务端返回一个包裹数据的函数callback(JSON)*/
至此这两个问题都解决了,是不是对JSONP为什么叫:被包裹的JSON有了一定的理解了;返回的数据就是一个被函数包裹起来的JSON;然而事情并没有这么简单就结束了,一个问题的解决往往意味着下个问题将要被发现;我们从服务端返回的函数 callback(JSON) 如果没有在客户端预先定义好也是会报错的;
那么定义函数的时候如何让后端也知道我们定义好的函数名是什么呢?也许你会想直接固定一个函数名,告诉后端不就好了,这样做只有一个JSONP请求还好,如果有很多JSONP你岂不是得一遍遍得告诉后端函数名,这样做怕是会挨揍的emm...;值得高兴的是我们在指定URL的时候可以向服务端发送一个callback值;如此一来我们就可以通过设置script的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 31 32 33 34 35 36 |
//php服务端 header("Content-Type:application/json;charset:utf-8"); if ($_GET['callback']=='jsp') { echo "{$_GET['callback']}({'name':'我是jsonp'})";//返回一个包裹json数据的函数jsp({'name':'我是jsonp'}) } //js客户端 function jsp(res){//定义jsp函数 console.log(res); } var cScript=document.createElement('script');//创建script标签 cScript.src='http://localtest/index.php?callback=jsp';//告诉后端我定义了函数名为jsp的函数让后端也返回这个函数名的函数 document.getElementsByTagName('body')[0].appendChild(cScript);//将script添加到html中 //////////////////////////////////////////////// //封装好的JSONP function jsonp(url,data,callback){ //设置随机函数名:防止多次调用前者被后者覆盖 var functionName='json_'+(+new Date())+Math.random().toString().slice(2,5); var script=document.createElement('script'); //判断有无data,如果有将其转换成name:222&...形式 if (typeof data==='object') { var tempArry=[]; for(var key in data){ tempArry.push(key+'='+data[key]) } data=tempArry.join('&'); }; script.src=url+"?callback="+functionName+"&"+data; document.body.appendChild(script); //相当于回调,向后端拿数据 window[functionName]=function(res){ callback(res); //用完之后移除 delete window[functionName]; document.body.removeChild(script); }; }; |
我们在开发页面的时候也会经常使用iframe标签进行页面嵌套,使用在页面嵌套的过程中会需要到页面之间的通信,不幸的是我们的页面有可能不在同一域下,这样一来页面之间通信就没那么容易了; ifrmae跨域需要父页面和子页面都是自己控制的,如果把iframe随便指向一个其他网站,想通过跨域手段操作它基本上是不可能的。 接下来介绍iframe相关的跨域方案
如果两个文件的主域名相同但是子域名不同,我们可以通过设置window.domain的来强制两个文件的基础主域相同,从而让两个文件互相通信
父窗口地址:http://aaa.test.com/test.html
子窗口地址:http://bbb.test.com/iframe.html
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
//父窗口test.html <body> <iframe id="myIframe" src="http://bbb.test.com/iframe.html"></iframe> <script> document.domain = 'test.com'; var b='222'; function load(){ conaole.log(document.getElementById('myIframe').contentWindow.a) } </script> </body> //子窗口iframe.html <body> <div id="test">xiaohuochai</div> <script> document.domain = 'test.com'; var a='111'; console.log(parten.window.b) </script> </body> |
window.name是一个很有意思的属性;它是一个所有浏览器都有的默认值是空字符串(只能是字符串)的全局属性;在你设置了window.name属性后再通过该页面打开同源或者非同源的其他页面后window.name的值是一样的;虽然在同一页面下请求不同地址window.name是一样的,但是想要访问不同源下的window.name属性是不可能的,emm...有点难理解;
我们可以将不同源的页面数据放到window.name值里,再创建一个同源的代理空页面,在父页面通过iframe标签的src引入不同源的页面,然后再通过改变iframe标签的src刷新引入同源的代理空页面,如此一来同源的代理空页面 的window.name和不同源的window.name的值一样了,父页面也能访问同源代理空页面的window.name了,emm...有点绕
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// a.html(http://localhost:80/a.html) a.html和b.html同源 <iframe src="http://localhost:88/c.html" frameborder="0" onload="load()" id="iframe"></iframe> <script> var flg = true; // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name function load() { if(flg){ // 第1次onload(跨域页)成功后,切换到同域代理页面 var oIframe = document.getElementById('iframe'); oIframe.src = 'http://localhost:80/b.html'; flg = false; }else{ // 第2次onload(同域b.html页)成功后,读取同域window.name中数据 console.log(oIframe.contentWindow.name); } } </script> // c.html(http://localhost:88/c.html) 和a.html不同源 <script> window.name = '这种跨域有点鸡肋' </script> |
location.hash就是url'http://wellwin.top#hhh'中的'#hhh',同一域下改变hash是不会导致页面刷新的(这个上一个window.name全局属性很像);因此我们可以把需要的数据放到hash中进行传递。然鹅,不同域是不能获取对方hash值的。我们可以像上一个window.name跨域方案一样创建一个和原页面同源的中间页面,来进行跨域
a.html和b.html同源(http://localhost:80) 而c.html(http://localhost:88);我们在a.html中引入c.html,再在c.html中引入b.html,然后在c.html中改变的hash值,再在监听hash值的变化,这样a.html就拿到了c.html传过来的hash值
(这里突然想到让c.html直接修改a.html的hash再由a.html监听hash变化岂不是简单粗暴,再一想如果随便把a.html值改变了岂不是会覆盖原先的数据emm...)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
// a.html 地址:http://localhost:80 <iframe src="http://localhost:88/c.html#iamchtml"></iframe>//引入c.html <script> window.onhashchange = function () { //检测hash值的变化 console.log(location.hash); } </script> // b.html 地址:http://localhost:80 与a.html同源的空页面 <script> window.parent.parent.location.hash = location.hash //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面 </script> c.html 地址:http://localhost:88 var cHash=location.hash; var oIframe = document.createElement('iframe'); oIframe.src = 'http://localhost:80/b.html#'+cHash;//引入b.html并传入hash值 document.body.appendChild(oIframe);//将b.html引入到c.html的iframe中 |
postMessage是html5 XMLRequest level2中的API,它是为数不多可以跨域的window属性之一;允许来自不同源脚本采用异步的方式进行跨文档、多窗口之间的有限通信
● postmessage()适用于以下场景
原页面和打开的新页面之间的通信
多窗口之间的通信
原页面与嵌套的iframe窗口之间的通信
● 语法:otherWindow.postMessage(message, targetOrigin, [transfer])
otherWindow : 要给发送信息的目标窗口对象;可以是window.frames的某个成员,也可以是window.open创建的那个窗口。
message : 发送到其他 窗口的数据 ;
targetOrigin : 指定接收信息的地址;"*"表示所有地址的域都可接收
transfer :(可选) 是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权;
● 这里我们还需要了解message事件的几个属性
event.data:传递过来的字符串或对象;
event.origin:发送该消息窗口的域名;
event. source:发送该消息的窗口对象;
举个栗子: http://localhost:80/a.html页面向http://localhost:88/b.html传递“我是a”,然后后者传回"我是b"。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// a.html 地址:http://localhost:80/a.html <iframe src="http://localhost:88/b.html" frameborder="0" id="frame" onload="load()"></iframe> //等它加载完触发一个事件 <script> function load() { var oIframe = document.getElementById('frame'); oIframe.contentWindow.postMessage('我是a', 'http://localhost:88'); //发送'我是a'给b.html window.onmessage = function(e) { //监控message,接受b.html返回的数据 console.log(e.data) //我是b } } </script> // b.html 地址:http://localhost:88/b.html window.onmessage = function(e) {//监控message console.log(e.data); //我是a e.source.postMessage('我是b', e.origin);//e.source是a.html的window,e.origin是a.html的域名地址 } |
WebSocket是什么,与HTTP又有什么关系,可以用来干什么:
1).WebSocket是HTML5持久化的一个网络通讯协议。2).他和HTTP一样都是基于TCP协议的应用层协议;HTTP协议的通信只能由客户端发起,使用''轮询"的方式不断地向服务端询问是否有信息更新,这样效率低又浪费资源。由此问题我们聪明的攻城狮们提出了WebSocket协议;WebSocket与HTTP有良好的兼容性,在建立连接的时候会借助HTTP,建立好连接之后客户端与服务端之间的通信便与HTTP无关了。 3).用来实现客户端与服务端双向通信; 是服务器推送技术的一种很好实现 ;同时也是一种跨域解决方案 ;
原生的WebSocket API使用起来比较麻烦,我们这里就不多做介绍。ws库很好地封装了webSocket接口,提供了更简单、灵活的接口(当然你也可以使用socket.io库,它相比ws库更健壮些,但是ws要比socket.io快的多并能跑更多的服务);接下来我们用node开启一个webSocket协议服务,并实现客户端与服务端的双工通信:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
//客户端 test.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> </body> <script> var socket=new WebSocket('ws://localhost:8080'); socket.onopen=function(){//onopen用于指定连接成功后的回调函数 socket.send('洞幺洞幺,我是客户端,收到请回答');//给服务端发信息 } socket.onmessage=function(e){//onmessage用于指定收到服务器数据后的回调函数 console.log(e.data);//打印服务端信息 } </script> </html> |
1 2 3 4 5 6 7 8 9 10 11 |
//服务端 test.js var express=require('express');//引用express var app=express(); var WebSocket=require('ws');//引用ws var local=new WebSocket.Server({port:8080});//开服务器 local.on('connection',function(ws){ ws.onmessage=function(e){ ws.send('洞拐洞拐,我是服务端,收到请回答');//给客端发送信息 console.log(e.data); } }) |
说到底跨域问题就是浏览器的同源策略引起的,服务端之间是不存在跨域的;那么我们是不是可以在本地专门起一个服务器来接收浏览器的请求信息,再由这个服务器把请求信息传递给我们目标服务器,然后目标服务器再将响应的数据发送给本地新起的那个服务器,新起的服务器再将数据传给本地的客户端;这样通过中间新起的这个本地服务器将请求送给目标服务器再将数据传回客户端就完成了跨域问题
接下来我们再来一个栗子来感受一下用nodejs做中间层跨域,其中需要的http模块的知识了,可以参考浅析nodejs的http模块
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//test.html 客户端 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> </body> <script> var myAjax=new XMLHttpRequest(); myAjax.open('POST','http://localhost:8080/test.js');//代理服务器地址 myAjax.setRequestHeader('Content-Type', 'application/json'); myAjax.send(); myAjax.onreadystatechange=function(){ if (this.readyState==4) { console.log(this.responseText); } } </script> </html> |
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 |
// server1.js 代理服务器(http://localhost:8080) var http = require('http'); var server = http.createServer(function (request, response){ response.writeHead(200, {//stp1:代理服务器接受客户端请求,设置CORS的首部字段 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', 'Access-Control-Allow-Headers': 'Content-Type' }); var options= {//stp2:将请求转发给目标服务器,设置目标服务器信息; host: '127.0.0.1', port: 8088, url: '/', method: request.method, headers: request.headers }; function resCallback(serverResponse) {//处理得到的数据 var body = ''; serverResponse.on('data', function (chunk) {//stp3:拿到目标服务器的数据 body += chunk; }) serverResponse.on('end', function() { console.log('The data is ' + body); response.end(body);//stp4:将得到的数据发给客户端 }) } http.request(options,resCallback).end() }) server.listen(8080,function(){ console.log('我是代理服务器 http://localhost:8080'); }) |
1 2 3 4 5 6 7 8 9 |
// server2.js 目标服务器(http://localhost:8088) var http = require('http'); var data = {'name':'我是目标服务器localhost:8088的数据,拿到了么'}; var server = http.createServer(function(request, response){ request.url === '/'? response.end(JSON.stringify(data)):null; }) server.listen(8088,function(){ console.log('我是目标服务器 http://localhost:8088') }) |
至此我们大致了解了以node作为中间层进行跨域访问的原理;但是我们为什么要用node做中间层呢?
node做中间层不仅仅只是用来跨域的,还有以下这些用途:
Nginx是一个高性能的HTTP和反向代理服务器,也是一个IMAP/POP3/SMTP 代理服务器 ;具有响应快,占用内存小,并发能力强,可靠性高, 支持7层负载均衡(将请求均匀转发至多台服务器上,通过部署多台相同服务的服务器,以减轻服务器的压力) 等优点;使用Nginx反向代理进行跨域只需要修改Nginx的配置就行,不用修改其他客户端服务端代码,支持所有浏览器不影响服务器性能,是最简单的跨域方式 。 下载Nginx,解压后双击nginx.exe,在nginx目录下运行命令行输入 nginx -v 出现版本号,则安装成功。
Nginx反向代理跟node中间件的跨域原理是一样的,用Nginx开一个代理服务器,设置允许客户端访问,反向代理目标服务器; 举个简单的栗子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
//简单配置nginx.conf文件 server { listen 81;#监听端口 server_name localhost;#服务器名字 location / { root E:/Nginx/nginx-1.17.8;//默认打开文件位置 index index.html index.htm; proxy_pass http://127.0.0.1:8080; #反向代理 add_header Access-Control-Allow-Origin *;#允许所有源访问 add_header Access-Control-Allow-Headers Content-Type;#设置请求头类型 } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
//index.html 地址:E:/Nginx/nginx-1.17.8/index.html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> </head> <body> </body> <script> var xhr=new XMLHttpRequest(); xhr.open('POST','http://localhost:81'); xhr.setRequestHeader('Content-Type','application/json'); xhr.send(); xhr.onreadystatechange=function(){ if (this.readyState==4&&this.status==200) { console.log(this.responseText,233) } } </script> </html> |
1 2 3 4 5 6 7 8 9 |
//node.js 地址:localhost:8080 var http = require('http'); var data={'name':'我是nginx跨域的目标服务器数据,拿到没'}; var server = http.createServer(function(request, response){ request.url === '/'? response.end(JSON.stringify(data)):null; }); server.listen(8080,function(){ console.log('Server is running at port 8080...'); }); |
没有最好只有更好,工作学习中还是要根据使用场景,再决定使用那种跨域才好;