JavaScript跨域总结

255 阅读8分钟

同源策略

同一协议、同一域名、同一端口号,如果这三个条件中有任何一条不满足,就不允许两个脚本进行交互。

跨域

定义:跨域,指的是浏览器不能执行其他网站的脚本。是由浏览器的同源策略造成的,是浏览器对js施加的安全限制。只要协议(http/https)、域名、端口有任何一个不同,都被当作是不同的域。

什么情况下会出现跨域

工程服务化后,不同职责的服务分散在不同的工程中,往往这些工程的域名是不同的,但一个需求可能需要对应到多个服务,这时便需要调用不同服务的接口,因此会出现跨域。

解决跨域的方案

JSONP

定义:

通过<script>标签引入一个js文件,这个js文件加载成功后会执行url参数中指定的函数,并且会把json数据作为参数传入

⚠️:jsonp是需要服务器端配合的

⚠️:jsonp支持get请求,不支持post请求

前端:前端设置好回调函数,并把回调函数当做请求url的参数.

<script>
    function getPrice(data){
    console.log(data);
    }
</script>
<script 
    type="text/javascript" 
    src="http://sdffw.b2b.com/getSupplyPrice?callback=getPrice&bcid=47296567">
</script>

后端: 服务端不再返回的是一个JSON格式的数据,而是返回一段JS代码,将JSON的数据以参数的形式传递到这个函数中,而函数的名称就是callback参数的值。

const url = require('url');
require('http').createServer((req, res) => {
    const data = {};
    const callback = url.parse(req.url, true).query.callback ;   
    res.writeHead(200)
    res.end(`${callback}(${JSON.stringify(data)})`)  
    *// 服务器收到请求后,解析参数,*
    *// 将callback(data)以字符串的形式返还数据,前端页面会将callback(data)作为js执行*
    *// 调用jsonpCallback(data)函数。*
}).listen(3000, '127.0.0.1');

callback是前后台约定的查询参数,服务器端返回一个能执行的js文件,这个js文件是调用callback对应的参数值即getPrice执行,并且返回对应的数据,我们可以在getPrice方法里面来处理返回的数据,最终返回的结果如下:

getPrice({
    "data":{"priceType":"0","unit":"斤"},
    "message":"价格获取成功!!!",
    "state":"1"
})

原理:

  • 声明一个全局回调函数,参数为服务端返回的 data。
  • 创建一个 script 标签,拼接整个请求 api 的地址(要传入回调函数名称如 ?callback=getInfo ),赋值给 script 的 src 属性
  • 服务端接受到请求后处理数据,然后将函数名和需要返回的数据拼接成字符串,拼装完成是执行函数的形式。(getInfo('server data'))
  • 浏览器接收到服务端的返回结果,调用了声明的回调函数。

封装一个JSONP:

function myJsonp(url,data,callback){        
    var fnName = 'myJsonp_' + Math.random().toString().replace('.','');
    //定义一个全局回调函数
    window[fnName] = callback;
    //初始化序列化参数
    var querystring = '';
    for(var attr in data){
        querystring += attr + '=' + data[attr] + '&';
    }
    // 动态创建script标签
    var script = document.createElement('script');
    //后台接受回调函数,并调用
    script.src = url + '?' + querystring + 'callback=' + fnName;
    
    //处理完毕之后,删除script标签,否则多次请求,页面会存在多个script标签
    script.onload = function(){    
        document.body.removeChild(script);
    }
    document.body.appendChild(script);
}
CORS(跨域资源共享)Cross-origin resource sharing

CORS的基本思想就是使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功还是失败。

这种方式的关键是后端进行设置,即是后端开启 Access-Control-Allow-Origin 为*或对应的 origin就可以实现跨域。

⚠️:CORS需要浏览器和服务器同时支持

⚠️:通常使用CORS时,浏览器会将异步请求分为简单请求和特殊请求两种

【简单请求】

只要同时满足以下两大条件,就属于简单请求:

(1) 请求方法是以下三种方法之一:

  • GET
  • HEAD
  • POST

(2)HTTP的头信息不超出以下几种字段:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

当浏览器发现发现的ajax请求是简单请求时,会在请求头中携带一个字段:Origin。

请求报文:

解析:Origin中会指出当前请求属于哪个域(协议+域名+端口)。服务会根据这个值决定是否允许其跨域。

如果服务器允许跨域,需要在返回的响应头中携带下面信息:

响应报文:

  • Access-Control-Allow-Origin:可接受的域,是一个具体域名或者*,代表任意
  • Access-Control-Allow-Credentials:是否允许携带cookie,默认情况下,cors不会携带cookie,除非这个值是true。

⚠️:如果跨域请求要想操作cookie,需要满足3个条件:

  • 服务的响应头中需要携带Access-Control-Allow-Credentials并且为true。
  • 浏览器发起ajax需要指定withCredentials 为true
  • 响应头中的Access-Control-Allow-Origin一定不能为*,必须是指定的域名

【特殊请求】

不符合简单请求的条件,会被浏览器判定为特殊请求。

例如:

1:使用了下面任一 HTTP 方法:(put、delete、connect、options、patch、trace)

2:人为设置了对 CORS 安全的首部字段集合之外的其他首部字段。该集合为:

Accept、Accept-Language、Content-Language、Content-Type (but note the additional requirements below)、DPR、Downlink、Save-Data、Viewport-Width、Width

3:Content-Type 的值不属于下列之一:

application/x-www-form-urlencoded、multipart/form-data、text/plain

⚠️:特殊请求会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。

一个“预检”请求的样板:

与简单请求相比,除了Origin以外,多了两个头:

  • Access-Control-Request-Method:接下来会用到的请求方式,比如PUT
  • Access-Control-Request-Headers:会额外用到的头信息

服务的收到预检请求,如果许可跨域,会发出响应:

除了Access-Control-Allow-Origin和Access-Control-Allow-Credentials以外,这里又额外多出3个头:

  • Access-Control-Allow-Methods:允许访问的方式
  • Access-Control-Allow-Headers:允许携带的头
  • Access-Control-Max-Age:本次许可的有效时长,单位是秒,过期之前的ajax请求就无需再次进行预检了

如果浏览器得到上述响应,则认定为可以跨域。

postMessage

postMessage是HTML5 XMLHttpRequest Level2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:

  • 页面和其打开的新窗口的数据传递

  • 多窗口之间消息传递

  • 页面与嵌套的iframe消息传递

  • 上面三个场景的跨域数据传递

用法:postMessage(data,origin)方法接受两个参数

  • data: html5规范支持任意基本类型或可复制的对象,但部分浏览器只支持字符串,所以传参时最好用JSON.stringify()序列化。
  • origin: 协议+主机+端口号,也可以设置为"*",表示可以传递给任意窗口,如果要指定和当前窗口同源的话设置为"/"。

1.)a.html:(www.domain1.com/a.html)

<iframe id="iframe" src="http://www.domain2.com/b.html" style="display:none;" onload="onload()"></iframe>
<script>       
    
    function onload() {
    var iframe = document.getElementById('iframe');
    var win = iframe.contentWindow; // 获取window对象
       win.postMessage('传值必须是字符串', '*') // 向不同域发送消息
    };
</script>

2.)b.html:(www.domain2.com/b.html)

<script>
    // 接收domain1的数据
    window.onmessage = function(e) { // 注册message事件接收消息
    e = e || event; // 获取事件对象
   alert(e.data);  //通过date属性得到传送的消息
    },;
</script>

Nginx代理跨域

跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。

实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。

nginx具体配置:


#proxy服务器
server {
    listen       81;
    server_name  www.domain1.com;

    location / {
        proxy_pass   http://www.domain2.com:8080;  #反向代理
        proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名
        index  index.html index.htm;

        # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用
        add_header Access-Control-Allow-Origin http://www.domain1.com;  #当前端只跨域不带cookie时,可为*
        add_header Access-Control-Allow-Credentials true;
    }
}
  • 前端代码示例:
var xhr = new XMLHttpRequest();

// 前端开关:浏览器是否读写cookie
xhr.withCredentials = true;

// 访问nginx中的代理服务器
xhr.open('get', 'http://www.domain1.com:81/?user=admin', true);
xhr.send();
WebSocket协议跨域

原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。

  1. 前端代码:
<div>user input:<input type="text"></div>
<script src="https://cdn.bootcss.com/socket.io/2.2.0/socket.io.js"></script>
<script>
var socket = io('http://www.domain2.com:8080');

// 连接成功处理
socket.on('connect', function() {
    // 监听服务端消息
    socket.on('message', function(msg) {
        console.log('data from server: ---> ' + msg); 
    });

    // 监听服务端关闭
    socket.on('disconnect', function() { 
        console.log('Server socket has closed.'); 
    });
});

document.getElementsByTagName('input')[0].onblur = function() {
    socket.send(this.value);
};
</script>

2)Nodejs socket后台:

var socket = require('socket.io');

// 启http服务
var server = http.createServer(function(req, res) {
    res.writeHead(200, {
        'Content-type': 'text/html'
    });
    res.end();
});

server.listen('8080');
console.log('Server is running at port 8080...');

// 监听socket连接
socket.listen(server).on('connection', function(client) {
    // 接收信息
    client.on('message', function(msg) {
        client.send('hello:' + msg);
        console.log('data from client: ---> ' + msg);
    });

    // 断开处理
    client.on('disconnect', function() {
        console.log('Client socket has closed.'); 
    });
});
Nodejs中间件代理跨域

通过启一个代理服务器,实现数据的转发,也可以通过设置cookieDomainRewrite参数修改响应头中cookie中域名,实现当前域的cookie写入,方便接口登录认证。

COOP、COEP