跨域问题的解决与具体实现

同源策略限制

跨域指的是当两个url的协议,域名,端口三者有一个不同,就会发生跨域。之所以会有跨域,是因为浏览器的同源策略限制。
同源策略限制了一个源的脚本与另一个不同源的脚本之间的交互,这是一种安全保障机制。
是否同源的判断,只要看两个源的url端口和域名是否相同,只要有一个不同,那就是不同源。

跨域的解决方法

jsonp


jsonp简单来说就是通过在html中插入一个script标签,利用其src属性来发起请求,通过在src中为callback添加对应的回调函数来达到对响应数据进行处理的目的

如下例子
发起请求的客户端

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script>
        let sendJSONP = function(url, callbackName) {
            // 新建script标签
            let script = document.createElement("script");
            // 为script标签添加请求送达的服务器url和得到响应的回调函数
            script.src = `${url}&callback=${callbackName}`;
            // 在head标签中插入script标签
            document.head.appendChild(script);
        };

        let getResponse = function(data) {
            // 打印响应信息
            console.log(`the response you get is "${data}"`);
        };

        // 发起请求,这里的url为目标url
        sendJSONP('url/?name=Mike &age=1', 'getResponse');
    </script>
</body>
</html>

服务端 node

const util = require('util'),
    http = require('http'),
    url = require('url');

let data = JSON.stringify({
    message: "I HAVE ALREADY RECEIVED"
});

http.createServer(function(req, res) {
    req = url.parse(req.url, true);
    if (!req.query.callback) 
        res.end();
    console.log(`name is ${req.query.name}and his age is ${req.query.age}`);
    res.writeHead(200, { 'Content-Type': 'appliction/javascript' })
    res.end(req.query.callback + "('" + data + "')")
}).listen(3000)

这里打开服务器,然后打开我们写的页面
在这里插入图片描述
可以看到服务端控制台已经打印出对应的内容
在这里插入图片描述
客户端也打印出相应的响应信息

通过jsonp我们可以跨域发送请求,而且这种方式兼容老的浏览器,但是要注意的是,jsonp只允许发送get请求,而且因为我们是将script标签插入html中,所以无法做到异步请求

CORS


CORS是一个w3c标准,全称是“跨域资源共享”,通过额外的HTTP头来告诉浏览器,让运行在一个origin上的Web应用被准许访问不同域的服务器上指定的资源。
在我们常见的请求中,有很多都是跨域访问的,如img的src属性经常访问不同域的资源,但是出于安全性的考虑,脚本发出的请求,例如XMLHttpRequest和Fetch API遵循同源策略,不能访问不同域的资源。
如果要在脚本中也能访问不同域的资源,可以在服务器中设置CORS接口,这里主要是要服务器和浏览器(兼容性)的支持,前端的请求不需要做出改变。

CORS分简单请求和非简单请求

简单请求

符合简单请求的条件
请求方式为下面三个其中一个

  • get
  • head
  • post

content-type的值仅限下面三个其中之一

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

对于简单请求,浏览器直接发出CORS请求,也即是说,会自动在请求中添加origin字段。
当服务器接收到请求时,发现发出Origin指定的源不在允许访问的列表内,在响应时就会返回一个没有Access-Control-Allow-Origin字段的正常HTTP响应。而浏览器发现没有Access-Control-Allow-Origin字段后,就会知道请求出错,会在控制台中提示。要注意的是,这种错误的状态码是200,所以无法通过状态码来判断是否正确。
在这里插入图片描述
在这里插入图片描述
可以看到,即使报错,状态码仍是200。

我们可以通过设置Access-Control-Allow-Origin来让请求得以跨域

举个使用node设置CORS白名单的例子
3000端口的服务端文件

const http = require('http'),
    url = require('url');

let data = JSON.stringify({
    message: "I HAVE ALREADY RECEIVED"
});

http.createServer(function(req, res) {
    // 设置http://localhost:3001可访问当前服务端的资源
    res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3001');
    req = url.parse(req.url, true);
    if (!req.query.callback) 
        res.end();
    console.log(`name is ${req.query.name}and his age is ${req.query.age}`);
    res.end(req.query.callback + "('" + data + "')")
}).listen(3000)

3001端口的服务端文件

const http = require('http');
const fs = require('fs');

http.createServer((req, res) => {
    const html = fs.readFileSync('test.html', 'utf-8');
    res.writeHead(200, {
        'Content-Type': 'text/html'
    });
    res.end(html);

}).listen(3001);

发起请求的页面

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <script>
        const xhr = new XMLHttpRequest();
        xhr.open('get', 'http://localhost:3000/?name=Mike &age=1')
        xhr.send()
    </script>
</body>

</html>

在这里插入图片描述
在这里插入图片描述
可以看到,这里已经有Access-Control-Allow-Origin字段了,而且也不报错了。

如果将

res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3001');

改为

res.setHeader('Access-Control-Allow-Origin', '*');

我们甚至可以在本地(‘file://’)获取服务端的资源

非简单请求

上面已经提及了简单请求是什么了,其他的请求为非简单请求。
对于非简单请求的CORS请求,会在正式请求前发送一个预请求,询问服务器发起请求的页面所在的域名是否在可访问服务器资源的名单中,如果在的话,正式发起请求,否则报错。

预请求的method为options,我们试着将上面的get请求改为put请求

 xhr.open('put', 'http://localhost:3000/?name=Mike &age=1')

看看控制台
在这里插入图片描述
可以看到,这里的method已经变成了OPTIONS了。

同样地,和上面配置允许访问的域名一样,这里我们可以通过设置put请求可访问资源来实现跨域,在3000端口的服务端文件加上一行代码设置

http.createServer(function(req, res) {
    res.setHeader('Access-Control-Allow-Origin', '*');
    // ----------下面一行是增加的设置-------------------
    res.setHeader('Access-Control-Allow-Methods','PUT')
    // ----------上面一行是增加的设置-------------------
    req = url.parse(req.url, true);
    if (!req.query.callback) 
        res.end();
    console.log(`name is ${req.query.name}and his age is ${req.query.age}`);
    res.end(req.query.callback + "('" + data + "')")
}).listen(3000)

重启服务器,再次刷新页面,可以发现不报错了,查看控制台network,发现发起了两个请求
在这里插入图片描述
在这里插入图片描述
而且我们可以看到,两个请求虽然名字一样,但是Request Method是不一样的,显然一个是预请求,另一个才是真正的请求。

此外,一旦第一次预请求成功确定该请求可访问服务器资源,后面的请求就不必再发起预请求了
修改代码如下

<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
</head>

<body>
    <!-- 点击发送请求 -->
    <button onClick="send()">button</button>

    <script>
        function send() {
            const xhr = new XMLHttpRequest();
            xhr.open('put', 'http://localhost:3000/?name=Mike &age=1')
            xhr.send()
        }
    </script>
</body>

</html>

除了第一次点击时会发起两个请求,后面点击都只会发起一个请求。
(这样说其实不太准确,应该是在预请求得到响应后,每次发起请求都不再发起预请求,如果这里快速点击按钮,可能会发起多个预请求)

document.domain


document.domain的处理方式,感觉就像是域名的缩减
www.first.hello.com
www.second.hello.com

这里两个网址的域名一个为first.hello.com,一个为second.hello.com

可以通过将first.hello.com和second.hello.com的document.domain设置为hello.com就可以做到发送请求时不跨域

window.postMessage()


通过postMessage,我们可以向目标窗口发送MessageEvent消息
MessageEvent四个主要属性

  1. message:类型
  2. data:发送的内容
  3. origin:调用postMessaged的窗口地址
  4. source:调用postMessage的窗口信息

postMessage可以在一个窗口的引用对象上使用,比如当我们在窗口A使用window.open打开一个窗口B后,在打开的窗口B就可以使用window.opener来调用postMessage,这样就可以在打开的窗口B中,将信息发送给不同域的窗口A

举个例子
在CSDN首页使用window.open打开百度页面

window.open("https://www.baidu.com")

在这里插入图片描述
然后在CSDN页面监听其他页面发送的信息,打印出监听的信息(e.data)

window.addEventListener('message',function(e){
    console.log(e.data);
},false)

在这里插入图片描述
在百度页面中,向CSDN首页发送信息

window.opener.postMessage({
    data:123
},"https://www.csdn.net/");

在这里插入图片描述
在CSDN首页的控制台中,可以看到已经有东西打印出来了
在这里插入图片描述

除了使用window.open打开的页面来实现之外,也可以通过iframe的contentWindow属性来实现父窗口向不同域的子窗口发送信息的目的。
假设页面中有这个iframe标签,src的指向和当前页面的地址不同域

<body>
    <iframe src="https://www.baidu.com" id="iframe" frameborder="0"></iframe>
</body>

在iframe窗口中设置监听

window.addEventListener('message',function(e){
    console.log(e.data);
},false)

在这里插入图片描述
回到top发送信息

document.getElementById('iframe').contentWindow.postMessage({data:123},'https://www.baidu.com')

在这里插入图片描述
这里打印出的{data:123}是在子页面中打印出来的

可以通过设置控制台选项来让控制台只显示一个窗口的内容
在这里插入图片描述
可以看到,的确是在子页面打印出的数据,子页面的确收到了不同域的父窗口的信息

发布了178 篇原创文章 · 获赞 12 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/zemprogram/article/details/102760167
今日推荐