来吧!跨域(几种简单的跨域实现)

535 阅读11分钟

什么叫做跨域

简单来说就是一个源的文档或者脚本访问另一个源的文档或者脚本(也可以说是交互)也就是不满足同源策略的两个源之间的交互

同源策略?

这是浏览器的一种机制,同源策略(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实现跨域拿数据

image.png

使用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端口已启动');
})

最后结果:

image.png 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服务已启动');
});

结果:

image.png 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)

结果:

image.png

下面我们来封装一个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>

结果:

image.png

总结: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-urlencodedmultipart/form-datatext/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>

未使用任何手段跨域之前:一定是请求不到数据的:(端口和域名都不一样)

image.png 再来看我们发请求的时候浏览器携带了那些信息(从下图可以看出:浏览器发现这次跨源AJAX请求是简单请求,就自动在头信息之中,添加一个Origin字段)

image.png

上面的头信息中,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使用的是*,也就是说任何人都能访问我这个服务器了,不安全。可以自己设置允许哪些源访问我的服务器

请求成功! image.png 到这里我们就先来捋一捋可以设置哪些跨域的字段操作:

  • 指定允许其他域名访问 '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>

来我们点击发请求看看浏览器会返回什么?

image.png 由于我们后端并没有设置允许请求的类型,所以会跨域。我们再看浏览器发送这个请求的时候做了什么?

image.png 嗯?发了两次请求????是的没错(来看看两次请求分别是什么)

image.png

image.png 在上面,第一个请求是预检请求

预检请求

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

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为预检请求(preflight) 浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

如果服务器同意了预检请求

image.png 就会去检查OriginAccess-Control-Request-MethodAccess-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服务已启动');
})

这时候我们的请求就成功了:

image.png

预检失败: 如果服务器否定了"预检"请求,会返回一个正常的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网站,以此来解决了跨域问题。

image.png

这个直接拿的别的作者的: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