什么叫做跨域
简单来说就是一个源的文档或者脚本访问另一个源的文档或者脚本(也可以说是交互)也就是不满足同源策略的两个源之间的交互
同源策略?
这是浏览器的一种机制,同源策略(Same origin policy)是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,则浏览器的正常功能可能都会受到影响。可以说Web是构建在同源策略基础之上的,浏览器只是针对同源策略的一种实现。
为什么要同源策略
同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收
什么是同源策略
我们来看看url的组成部分 www.zjx.com:80/path/myfile…
- http:// :这一部分是浏览器的协议(protocal)部分
- www.zjx.com :这一部分是域名部分
- :80 : 这一部分是端口(port)部分
- /path/myfile :这一部分是请求路径(pathname)部分
- ?key=value1&name=zjx :这一部分是请求参数(search)部分 上面这些都可以用localtion.xxx去获得,比如要获取请求参数:localtion.search。会返回?key=value1&name=zjx
同源策略指的是:协议(protocal),端口(port),域名三者都要相同,才算得上同源,只要有一个不同就不是同源,就会产生跨域。端口一般不写(可省略)。
我们来看看下面哪些是同源哪些不是同源 (www.zjx.com:80/path/myfile…
-
http://www.zjx.com/path/kkk.html
:同源 -
http://www.kkk.com/path/kkk.html
:不同源(域名不同) -
http://v2.www.kkk.com/path/kkk.html
:不同源(域名不同) -
http://www.kkk.com:81/path/kkk.html
:不同源(端口不同) -
https://www.kkk.com/path/kkk.html
:不同源(协议不同)
下面我们来看看如何解决跨域
1.JSONP
JSONP是怎么进行跨域的呢?
SONP实现跨域请求的原理简单的说,就是动态创建<script>
标签,然后利用<script>
的src 不受同源策略约束来跨域获取数据
我们先来看看用ajax发一条原生请求:
<script text="text/javascript">
window.onload = function() {
var Btn = document.getElementById('btn');
//利用ajax发请求,当然也可以用别的方式
Btn.onclick = function() {
//创建一个XMLHttpRequest实例
var xhr = new XMLHttpRequest();
//通过 XMLHttpRequest 对象,您可以定义当请求接收到应答时所执行的函数
xhr.onreadystatechange = function() {
if (xhr.status == 200) {
alert('请求成功')
}
}
//向服务端放松请求的两个方法,open和send
//open接收三个参数,open(method,url,true)
//method:请求方法。url:请求地址。第三个参数是使用同步还是异步
xhr.open('get', 'http://localhost:4000', true)
xhr.send()
}
}
</script>
当我们点击发请求后:毫无疑问跨域了,那下面我们就使用jsonp实现跨域拿数据
使用jsonp的前端:
<button id="btn">点击发请求</button>
<!-- 直接使用script标签处理 -->
<script>
function show(data) {
console.log(data);
}
var btn=document.getElementById('btn')
btn.addEventListener('click',function(){
//创建一个script标签
let script = document.createElement('script')
script.src='http://localhost:4000?callback=show'
document.body.appendChild(script)
})
</script>
node版本: 后端使用hhtp创建一个服务
const http = require('http');
const querystring = require('querystring')
var data = {
status: 200,
message: '听闻爱情,十有九悲'
}
const server = http.createServer()
server.on('request', function (req, res) {
//以下三步都是截取前端传过来的参数
const kkk = req.url.split('?')[1]
const jjj = kkk.split('=')[1]
const callback = jjj
res.end(`${callback}(${JSON.stringify(data)});`);
}
)
server.listen(4000, function () {
console.log('4000端口已启动');
})
最后结果:
koa版本
const Koa = require('koa');
const app = new Koa();
var data = {
status: 200,
message: '听闻爱情,十有九悲,可你仍然相信爱情'
}
// response
app.use(ctx => {
//以下三部都是截取前端传过来的参数
const kkk = ctx.url.split('?')[1]
const jjj = kkk.split('=')[1]
const callback = jjj
ctx.body = `${callback}(${JSON.stringify(data)});`
});
app.listen(4000,function(){
console.log('4000服务已启动');
});
结果:
express版本
const express = require('express')
const app = express()
var data = {
status: 200,
message: '听闻爱情,十有九悲,可你仍然相信爱情,而我依然爱你'
}
app.get('/', function (req, res) {
var callback=req.query.callback
res.send(`${callback}(${JSON.stringify(data)});`)
})
app.listen(4000)
结果:
下面我们来封装一个jsonp
<script>
function jsonp({ url, params, callback }) {
let dataStr = ''
//把params的参数拿下来(parmas通常是个对象)
for (let key in params) {
dataStr += `${key}=${params[key]}&`
}
//把回调函数也拼接到dataStr
dataStr += `callback=${callback}`
//创建script标签
let script = document.createElement('script')
script.src = `${url}?${dataStr}`
console.log(script.src);
document.body.appendChild(script)
// 执行回调函数:(方案1)
// window[callback] = (data) => {
// console.log(data);
// }
// 方案二:
return new Promise((resolve, reject) => {
window[callback] = data => {
resolve(data)
document.body.removeChild(script)
}
})
}
jsonp({
url: 'http://localhost:4000',
params: {
name: 'zjx',
age: 18
},
callback: 'show'
}).then(data=>{
console.log(data);
})
</script>
结果:
总结:jsonp只支持get请求(局限性太大)
2.cors(一般在后端做)
简单介绍一下cors:CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
整个CORS通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS通信与同源的AJAX通信没有差别,代码完全一样。浏览器一旦发现AJAX请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现CORS通信的关键是服务器。只要服务器实现了CORS接口,就可以跨源通信。
分为简单请求和非简单请求
首先我们得知道什么是简单请求,知道了简单请求剩下的就是非简单请求
只要同时满足以下两大条件,就属于简单请求
(1) 请求方法是以下三种方法之一:
- get
- post
- head (2)HTTP的头信息不超出以下几种字段:
- Accept 设置接受的内容类型(请求头)
- Accept-Language 设置接受的语言(请求头)
- Content-Language 为封闭内容设置自然语言或者目标用户语言(响应头)
- Last-Event-ID
- Content-Type:设置请求体的MIME类型(适用POST和PUT请求))只限于三个值
application/x-www-form-urlencoded
、multipart/form-data
、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
浏览器对这两种请求的处理,是不一样的。
我们先来看看简单请求如何使用cors
后端:
const http = require('http')
const data={
status:200,
message:'使用cors跨域'
}
const server=http.createServer(function(req,res){
// res.writeHead(200,{
// // "Content-Type":"text/plain", //设置请求头的类型
// "Access-Control-Allow-Origin":"*" , //表示允许所有的域来请求我
// // "Access-Control-Allow-Headers":"",
// })
res.end(JSON.stringify(data))
})
server.listen(4000,()=>{
console.log('4000服务已启动');
})
我们前端向后端发起一个请求:
<Button id='btn'>点击发送请求</Button>
<script>
var btn=document.getElementById('btn')
btn.addEventListener('click',()=>{
// 创建一个实例
var xhr=new XMLHttpRequest()
xhr.onreadystatechange=function (res){
if(xhr.status==200 && xhr.readyState==4){
alert('请求成功')
console.log(xhr.responseText);
}
}
xhr.open('get','http://localhost:4000')
xhr.send()
})
</script>
未使用任何手段跨域之前:一定是请求不到数据的:(端口和域名都不一样)
再来看我们发请求的时候浏览器携带了那些信息(从下图可以看出:浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin
字段)
上面的头信息中,Origin
字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。所以这时候我们就要开启服务器的头设置:看代码
const http = require('http')
const data={
status:200,
message:'使用cors跨域'
}
const server=http.createServer(function(req,res){
res.writeHead(200,{
// "Content-Type":"text/plain", //设置请求头的类型
"Access-Control-Allow-Origin":"*" , //表示允许所有的域来请求我
// "Access-Control-Allow-Headers":"",
})
res.end(JSON.stringify(data))
})
server.listen(4000,()=>{
console.log('4000服务已启动');
})
这时候我们去请求就能请求成功了!!但是呢我们的origin使用的是*,也就是说任何人都能访问我这个服务器了,不安全。可以自己设置允许哪些源访问我的服务器
请求成功! 到这里我们就先来捋一捋可以设置哪些跨域的字段操作:
- 指定允许其他域名访问 'Access-Control-Allow-Origin:http://172.20.0.206'
- 一般用法(,指定域,动态设置),3是因为不允许携带认证头和cookies//是否允许后续请求携带认证信息(cookies),该值只能是true,否则不返回 'Access-Control-Allow-Credentials:true'
- 预检结果缓存时间,也就是上面说到的缓存啦 'Access-Control-Max-Age: 1800'
- 允许的请求类型 'Access-Control-Allow-Methods:GET,POST,PUT,POST'
- 允许的请求头字段 'Access-Control-Allow-Headers:x-requested-with,content-type'
再来看看非简单请求
上面改下代码:把get请求方法改为put
<Button id='btn'>点击发送请求</Button>
<script>
var btn=document.getElementById('btn')
btn.addEventListener('click',()=>{
// 创建一个实例
var xhr=new XMLHttpRequest()
xhr.onreadystatechange=function (res){
if(xhr.status==200 && xhr.readyState==4){
alert('请求成功')
console.log(xhr.responseText);
}
}
xhr.open('put','http://localhost:4000')
xhr.send()
})
</script>
来我们点击发请求看看浏览器会返回什么?
由于我们后端并没有设置允许请求的类型,所以会跨域。我们再看浏览器发送这个请求的时候做了什么?
嗯?发了两次请求????是的没错(来看看两次请求分别是什么)
在上面,第一个请求是预检请求
预检请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者Content-Type
字段的类型是application/json
。
非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为预检请求(preflight)
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest
请求,否则就报错。
如果服务器同意了预检请求
就会去检查Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应。也就是说当我们给后端配置上上面三个:
const http = require('http')
const data={
status:200,
message:'使用cors跨域'
}
const server=http.createServer(function(req,res){
res.writeHead(200,{
// "Content-Type":"text/plain", //设置请求头的类型
"Access-Control-Allow-Origin":'*' , //表示允许所有的域来请求我
"Access-Control-Allow-Headers":"ontent-type",
"Access-Control-Allow-Methods":"PUT",
})
res.end(JSON.stringify(data))
})
server.listen(4000,()=>{
console.log('4000服务已启动');
})
这时候我们的请求就成功了:
预检失败:
如果服务器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest
对象的onerror
回调函数捕获。控制台会打印出如下的报错信息。
XMLHttpRequest cannot load http://api.alice.com.
Origin http://api.bob.com is not allowed by Access-Control-Allow-Origin.
比较一下jsonp和cors CORS与JSONP的使用目的相同,但是比JSONP更强大。
JSONP只支持GET
请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。
这里推荐一篇文章:http://www.ruanyifeng.com/blog/2016/04/cors.html
3.postmessage
首先我们得清楚postmessage的用法:先看看语法:
otherWindow.postMessage(message, targetOrigin, [transfer]);
otherWindow:其他窗口的一个引用,比如 iframe 的 contentWindow 属性、执行 window.open 返回的窗口对
象、或者是命名过或数值索引的 window.frames。
message:将要发送到其他窗口(window)的数据。
targetOrigin:指定哪些窗口能接收到消息事件,其值可以是 *(表示无限制)或者一个 URI。
transfer:可选,是一串和 message 同时传递的 Transferable 对象。这些对象的所有权将被转移给消息的接收
方,而发送一方将不再保有所有权
举个例子:我们首先有一个a页面
a页面要做哪些事呢?
假设a页面要接收b页面的数据。那我们在a页面就是去打开一个新的页面,并且要给这个页面添加一个message的监听事件。来我们看看a页面做的事
a页面
<script>
function test() {
let op = window.open('b.html', '_blank');
function receiveMessage(event) {
console.log('event', event.data);
}
op.addEventListener("message", receiveMessage, false);
}
</script>
那么b页面是不是就要把a要的数据传过来,看看b页面干什么(b页面要用postmessage把数据传过来)(本地打开页面没有用,用liveserver打开才行)
<script>
写法1
// function post() {
// window.postMessage("hi there!", location.origin);
// }
// function receiveMessage(event) {
// console.log('event', event.data)
// }
// window.addEventListener("message", receiveMessage, false);
写法二
window.onload = function post() {
window.postMessage("我是你请求的数据", location.origin);
}
</script>
使用postmessage的页面发送的数据会发送到所有的同源页面(除非指定页面)包括自己这个页面也会收到这个数据
4.nginx
我没有用过nginx代理:大概的意思就是你这个请求让别人代替你去发送
A网站向B网站请求1.js文件时,向B网站发送一个获取的请求,nginx根据配置文件接收这个请求,代替A网站向B网站来请求这个资源,nginx拿到这个资源后再返回给a网站,以此来解决了跨域问题。
这个直接拿的别的作者的:https://juejin.cn/post/6947940375008903176#heading-13
5.webscoket
全双工双向通信(即服务端和客户端都可发送信息)
有哪些特点呢?
(1)建立在 TCP 协议之上,服务器端的实现比较容易。
(2)与 HTTP 协议有着良好的兼容性。默认端口也是80和443,并且握手阶段采用 HTTP 协议,因此握手时不容易屏蔽,能通过各种 HTTP 代理服务器。
(3)数据格式比较轻量,性能开销小,通信高效。
(4)可以发送文本,也可以发送二进制数据。
(5)没有同源限制,客户端可以与任意服务器通信。
(6)协议标识符是ws
(如果加密,则为wss
),服务器网址就是 URL。
来我们看看怎么使用webscoket
首先我们先来创建一个webscoket服务:创建服务,指定8181端口,将收到的消息log出来。
var WebSocketServer = require('ws').Server,
wss = new WebSocketServer({ port: 8181 });
wss.on('connection', function (ws) {
console.log('client connected');
ws.on('message', function (message) {
console.log(message);
ws.send('您好')
});
});
再看看我们的客户端:
<script type="text/javascript">
//与后端建立一个webscoket的连接
var ws = new WebSocket('ws://localhost:8181');
ws.onopen = function () {
console.log('ws onopen');
ws.send('from client: hello');
};
ws.onmessage = function (e) {
console.log('ws onmessage');
console.log('from server: ' + e.data);
};
</script>
客户端的 API
WebSocket 构造函数
WebSocket 对象作为一个构造函数,用于新建 WebSocket 实例。
var ws = new WebSocket('ws://localhost:8181');
执行上面语句之后,客户端就会与服务器进行连接。
webSocket.readyState
readyState
属性返回实例对象的当前状态,共有四种。
- CONNECTING:值为0,表示正在连接。
- OPEN:值为1,表示连接成功,可以通信了。
- CLOSING:值为2,表示连接正在关闭。
- CLOSED:值为3,表示连接已经关闭,或者打开连接失败。
webSocket.onopen
实例对象的onopen
属性,用于指定连接成功后的回调函数。
ws.onopen = function () {
console.log('ws onopen');
ws.send('from client: hello');
};
webSocket.onclose
实例对象的onclose
属性,用于指定连接关闭后的回调函数。
// ws.onclose = function (evt) {
// console.log("Connection closed.");
// };
webSocket.onmessage
实例对象的onmessage
属性,用于指定收到服务器数据后的回调函数。
ws.onmessage = function (e) {
console.log('ws onmessage');
console.log('from server: ' + e.data);
};
webSocket.send()
实例对象的send()
方法用于向服务器发送数据。
ws.send('your message');
这篇文章详细:http://www.ruanyifeng.com/blog/2017/05/websocket.html