跨域是我们在项目中经常遇到的,前后端数据交互经常碰到请求跨域,首先我们来想一下为什么会有跨域这个词的出现?本文带你来探讨一下以下几个问题:
- 跨域是什么?
- 为什么要跨域?
- 跨域的几种方式?
- ...
什么是跨域?
跨域
是指的浏览器不能执行其它网站的脚本,它是由浏览器的同源策略
造成,是浏览器对JavaScript实施的安全限制。
跨域
实际上指从一个域的网页去请求另一个域的资源,比如:从 http://www.baidu.com
网站去请求http://www.google.com
网站的资源。
什么是同源策略?
同源策略
指的是 域名,协议,端口 三者都相同~
什么是同源?
要知道URL
由协议,域名,端口以及路径组成,若两个URL
的协议、域名和端口相同,则表示他们同源。
相反,只要协议,域名,端口有任何一个的不同,就被当作是跨域。
限制同源策略内容
- Cookie、LocalStorage、IndexedDB等存储性内容
- DOM节点
- Ajax请求发送后,结果被浏览器拦截了
允许跨域加载资源
这下边三个含有 src
标签的是允许跨域加载资源的
<img src=XXX>
<link href=XXX>
<script src=XXX>
跨域的场景
九种跨域解决方案
- jsonp
- cors
- postMessage
- document.domain
- window.name
- location.hash
- https-proxy
- nginx
- websocket
jsonp
什么是jsonp
jsonp
全称是JSON with Padding
,是为了解决跨域请求资源而产生的解决方案,是一种依赖开发人员创造出的一种非官方跨域数据交互协议。
Jsonp的原理
- 利用script标签的src属性来实现跨域
- 通过将前端方法作为参数传递到服务器端,然后由服务器端注入参数之后再返回,实现服务器端向客户端通信
- 由于使用script标签的src属性,因此只支持get方法
Jsonp和Ajax对比
- Jsonp和Ajax相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式
- Ajax属于同源策略
- Jsonp属于非同源策略(跨域请求)
Jsonp的优缺点
优点:
- 它不像XMLHttpRequest对象实现的Ajax请求那样受到同源策略的限制,JSONP可以跨越同源策略
- 它的兼容性更好,在更加古老的浏览器中都可以运行,不需要XMLHttpRequest或ActiveX的支持
- 在请求完毕后可以通过调用callback的方式回传结果
缺点:
- 它只支持GET请求而不支持POST等其它类型的HTTP请求
- 它只支持跨域HTTP请求这种情况,不能解决不同域的两个页面之间如何进行JavaScript调用的问题
- jsonp在调用失败的时候不会返回各种HTTP状态码
- 缺点是安全性,万一假如提供jsonp的服务存在页面注入漏洞,即它返回的javascript的内容被人控制的
Jsonp的实现流程
- 声明一个回调函数,把函数名(show)当做参数值
- 要传递给跨域请求的数据的服务器,函数形参为要获取目标数据
- 创建一个script标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名
- 服务器接收到请求后,需要进行处理:把传递的参数名和它需要的数据拼接成一个字符串
- 最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作
具体代码实现如下:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script>
function jsonp({url,params,cb}){
return new Promise((resolve,reject)=>{
let script = document.createElement('script');
window[cb]=function(data){
resolve(data);
document.body.removeChild(script);
}
params={...params,cb}//wd=b&cb=show
let arrs = [];
for(let key in params){
arrs.push(`${key}=${params[key]}`);
}
script.src = `${url}?${arrs.join('&')}`;
document.body.appendChild(script)
})
}
//只能发送get请求,不支持post put delete
//不安全xss攻击 不采用
jsonp({
url:'http://localhost:3000/say',
params:{wd:'早上好'},
cb:'show'
}).then(data=>{
console.log(data)
})
</script>
</body>
</html>
serve.js
let express = require('express');
let app = express();
app.get('/say',function (req,res){
let {wd,cb} = req.query;
console.log(wd);
res.end(`${cb}('晚上好')`)
})
app.listen(3000)
注意: 需要安装npm install express
, 然后在终端里面输入node serve.js
, 再把index.html
在浏览器上边console
栏查看返回结果
JQuery的jsonp跨域请求
如果从 192.168.19.1
发ajax
请求到 192.168.19.6
会产生跨域问题, 利用jquery
的jsonp
参数可轻松这个问题。
注意:Jsonp都是GET和异步请求
function get() {
$.ajax({
type: "GET",
url: 'http://192.168.19.6:8080/jsgd/bill.jsp?userCode=?&date='+ new Date(),
dataType:"jsonp",
jsonp:"jsonpcallback",
success: function(msg){
$('#callcenter').html(msg.text);
}
});
}
cors
什么是cors
cors
全称"跨域资源共享"(Cross-origin resource sharing), 是一种ajax跨域请求资源的方式。
兼容性
- cors需要浏览器和服务器同时支持,才可以实现跨域的请求
- 这个方法几乎所有的浏览器都支持,但是ie必须是10以上
- ie8和9需要通过XDomainRequest来实现
请求类型
cors分为简单请求
和复杂请求
两类
简单请求
请求方式使用下列方法之一:
GET
HEAD
POST
Content-Type的值仅限于下列三者之一:
text/plain
multipart/form-data
application/x-www-form-urlencoded
注意:对于简单的请求,浏览器会直接发送cors请求,具体来说就是在header中加入origin请求头字段。在响应头回服务器设置相关的cors请求,响应头字段为允许跨域请求的源。请求时浏览器在请求头的Origin中说明请求的源,服务器收到后发现允许该源跨域请求,则会成功返回。
复杂请求
使用了下面任一HTTP方法
PUT
DELETE
CONNECT
OPTIONS
TRACE
PATCH
Content-Type的值不属于下列之一:
application/x-www-form-urlencoded
multipart/form-data
text/plain
当符合复杂请求的条件时,浏览器会自动先发送一个options请求。如果发现浏览器支持该请求,则会将真正的请求发送到后端。如果浏览器发现服务端不支持该请求,则会在控制台抛出错误。
cors字段介绍
- Access-Control-Allow-Methods
这个字段是必要的,它的值是逗号分割的一个字符串,表明服务器支持的所有跨域请求的方式
-
Access-Control-Allow-Headers
如果浏览器请求包括这个字段,则这个字段也是必须的,它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在“预检"中请求的字段
- Access-Control-Allow-Credentials
这个字段与简单请求时的含义相同
- Access-Control-Allow-Credentials
- Access-Control-Max-Age
该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求
流程实现
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
hello
</body>
</html>
serve.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
以当前这个作为静态文件目录,先要在终端里面node serve.js
服务器打开,访问localhost:3000
就可以把 hello 显示出来。
这是一个完整的复杂请求例子:
index.js
let xhr = new XMLHttpRequest()
document.cookie = 'name=xiaoming'
xhr.withCredentials = true
xhr.open('PUT', 'http://localhost:4000/getData', true)
xhr.setRequestHeader('name', 'xiaoming')
xhr.onreadystatechange = function() {
if (xhr.readyState === 4) {
if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) {
console.log(xhr.response)
//得到响应头,后台需设置Access-Control-Expose-Headers
console.log(xhr.getResponseHeader('name'))
}
}
}
xhr.send()
serve.js
let express = require('express');
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
serve2.js
let express = require('express')
let app = express()
let whitList = ['http://localhost:3000']
app.use(function(req, res, next) {
let origin = req.headers.origin
if (whitList.includes(origin)) {
// 设置哪个源可以访问我
res.setHeader('Access-Control-Allow-Origin', origin)
// 允许携带哪个头访问我
res.setHeader('Access-Control-Allow-Headers', 'name')
// 允许哪个方法访问我
res.setHeader('Access-Control-Allow-Methods', 'PUT')
// 允许携带cookie
res.setHeader('Access-Control-Allow-Credentials', true)
// 预检的存活时间
res.setHeader('Access-Control-Max-Age', 6)
// 允许返回的头
res.setHeader('Access-Control-Expose-Headers', 'name')
if (req.method === 'OPTIONS') {
res.end() // OPTIONS请求不做任何处理
}
}
next()
})
app.put('/getData', function(req, res) {
console.log(req.headers)
res.setHeader('name', 'ming') //返回一个响应头,后台需设置
res.end('早上好')
})
app.get('/getData', function(req, res) {
console.log(req.headers)
res.end('早上好')
})
app.use(express.static(__dirname))
app.listen(4000)
Cors与Jsonp比较
- cors比Jsonp更强大
- Jsonp只支持Get请求,cors支持所有类型的HTTP请求
- 使用cors,可以使用XMLHttpRequest发起请求和获取数据,比Jsonp有更好的错误处理
- Jsonp的优势在于支持老式浏览器和可以向cors的网络请求数据
- cors与Jsonp相比,更方便可靠
postMessage
什么是postMessage
postMessage
方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。
postMessage语法
otherWindow.postMessage(message, targetOrigin, [transfer])
- otherWindow:其它窗口(目标窗口)的引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames
- message:将要发送到其他window的数据,这个数据会自动被序列化,数据格式也不受限制(字符串,对象都可以)
- targetOrigin:目标窗口的源,可以是字符串*表示无限制,或URL,需要协议端口号和主机都匹配才会发送
- transfer(可选):是一串和message同时传递的Tranferable对象,这些对象的所有权将 被转移给消息接收方,而发送一方将不再保有所有权
兼容性
高级浏览器Internet Explorer 8+
, chrome
,Firefox
, Opera
和 Safari
都将支持这个功能
流程实现
a.html 向 b.html传递 "早上好",然后 a.html 传回"今天天气真好"
a.html
<iframe src="http://localhost:4000/b.html" frameborder="0" id="frame" onload="load()"></iframe>
<script>
function load(){
let frame = document.getElementById('frame');
frame.contentWindow.postMessage('早上好','http://localhost:4000');
window.onmessage=function(e){
console.log(e.data)
}
}
</script>
b.html
<script>
window.onmessage = function(e){
console.log(e.data);
e.source.postMessage('今天天气不错',e.origin)
}
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)
Window.name
什么是Window.name
window.name
是一个 window
对象的内置属性,name
属性可以设置或返回存放窗口的名称的一个字符串。
该属性的特征
在页面在浏览器端展示的时候,我们总能在控制台拿到一个全局变量window,该变量有一个name属性,有以下的特征:
- 每个窗口都有独立的window.name与之对应
- 在一个窗口的生命周期中(被关闭前),窗口载入的所有页面同时共享一个window.name,每个页面window.name都有读写的权限
- window.name一直存在与当前窗口,即使是新的页面载入也不会改变window.name的值
- window.name可以存储不超过2M的数据,数据个数按需自定义
流程实现
- a.html和b.html是同域
http://localhost:3000
- c.html是独立的
http://localhost:4000
- a获取c的数据
- a先引用c
- c把值放到
window.name
,把a引用的地址改为b
a.html
<iframe src="http://localhost:4000/c.html" frameborder="0" onload="load()" id="iframe"></iframe>
<script>
let first = true
function load(){
if(first){
let iframe = document.getElementById('iframe');
iframe.src="http://localhost:3000/b.html";
first = false;
}else{
console.log(iframe.contentWindow.name)
}
}
<script>
b.html
<body>
早上好
</body>
c.html
<script>
window.name='今天天气不错'
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)
location.hash
什么是location.hash
location
是javascript
里面的内置对象,如location.href
就管理页面的url,用loaction.href=url
就可以直接将页面重定向url,而location.hash
则可以用来获取或设置页面的标签值,hash属性是一个可读可写的字符串,该字符串是URL的锚部分(从#号开始的部分)
location.hash的简单应用
#
的含义
#
代表网页中的位置,其右边的字符,就是该位置的标识符,例如:
http://www.juejin.com/index.html#drafts
就是代表index.html的drafts位置,浏览器读取这个URL后,会自动将print位置滚动至可视区域
HTTP请求不包括#
#是用来指导浏览器的动作的,对服务器端完全无用,所以,HTTP请求中不包括#
例如:
http://www.juejin.com/index.html#drafts
浏览器实际发出的请求是这样的:
GET/index.html HTTP/1.1
Host:www.juejin.com
可以看到,只是请求的index.html,没有#drafts部分
#
后的字符
在第一个#出现的任何字符,都会被浏览器解读为位置标识符,这意味着,这些字符不会被发送到服务器端
改变#不触发网页重构
单单改变#后的部分,浏览器只会滚动到相应的位置,不会重新加载网页
改变#会改变浏览器的访问历史
每一次改变#后的部分,都会在浏览器的访问历史中增加一个记录,使用"后退"按钮,就可以回到上一个位置
读取#值
window.location.hash这个属性可读可写。读取时,可以用来判断网页状态是否改变;写入时,则会在不重载网页的前提下,创造一条访问历史记录
onhashchange事件
当#值发生变化时,就会触发这个事件。IE8+、Firefox 3.6+、Chrome 5+、Safari 4.0+支持该事件
Google抓取#的机制
默认情况下,Google的网络忽视URL的#部分
流程实现
路径后面的hash值可以用来通信。目的:a.html
想访问 c.html
跨域相互通信。
- a.html给c.html传一个hash值,需要通过中间的b.html来实现
- c.html收到hash值后 c.html把hash值传递给b.html
- b.html将结果放到a.html的hash值中
a.html
<iframe src="http://localhost:4000/c.html#goodmorning"></iframe>
<script>
window.onhashchange = function () {
console.log(location.hash);
}
</script>
b.html
<script>
window.parent.parent.location.hash = location.hash
</script>
c.html
<script>
console.log(location.hash);
let iframe = document.createElement('iframe');
iframe.src = 'http://localhost:3000/b.html#goodevening';
document.body.appendChild(iframe);
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000)
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000)
domain
什么是domain
主要用于主域相同的域之间的数据通信,注意 仅限主域相同,子域不同的跨域应用场景。
实现的原理:两个页面都通过js强制设置 document.domain
为基础主域,就实现了同域
说明
这个方法只能用于二级域名相同的情况下,比如:
www.baidu.com
hhh.baidu.com
这就适用于domain方法
流程实现
a.html
<iframe src="http://b.ming.cn:3000/b.html" frameborder="0" onload="load()" id="frame"></iframe>
<script>
document.domain = 'ming.cn'
function load() {
console.log(frame.contentWindow.a);
}
</script>
b.html
<div>
早上好啊
</div>
<script type="text/javascript">
document.domain = 'ming.cn'
var a = 99999;
</script>
a.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(3000, () => {
console.log('server run at 3000')
})
b.js
let express = require('express')
let app = express();
app.use(express.static(__dirname));
app.listen(4000, () => {
console.log('server run at 4000')
})
这个就可以通过http://a.ming.cn:3000/a.html获取到页面http://a.ming.cn:3000/b.htm中的a的值99999
注意:这里我把我电脑上边的hosts修改了一下,不然不能出来效果
WebSocket
什么是WebSocket
WebSocket
是一种网络通信协议,它实现了浏览器与服务器全双工通信,同时允许跨域通讯。原生WebSocket API使用起来不太方便,我们使用Socket.io
,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。
WebSocket如何工作
Web浏览器和服务器都必须实现 WebSockets 协议来建立和维护连接。由于 WebSockets 连接长期存在,与典型的HTTP连接不同,对服务器有重要的影响。
注意:基于多线程或多进程的服务器无法适用于WebSockets
,因为它旨在打开连接,尽可能快地处理请求,然后关闭连接。任何实际的WebSockets
服务器端实现都需要一个异步服务器。
流程实现
a.html
<script>
//高级api 不兼容 socket.io(一般使用它)
let socket = new WebSocket('ws://localhost:3000');
socket.onopen=function(){
socket.send('早上好啊')
}
socket.onmessage = function(e){
console.log(e.data);
}
</script>
a.js
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){
console.log(data)
ws.send('今天天气真好')
})
})
总结
以上就是整理的一些跨域的方法,我觉得一般用cors,jsonp等常见的方法就可以了,不过遇到了一些特殊情况,我们也要做到有很多方法是可以选择的,相信这篇文字会对大家有帮助!
欢迎大家加入❤️❤️❤️
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。