Loading

跨域的几种实现方式

首先,浏览器为了保护你的安全,禁止一个网站向和它不同域名的网站发送ajax请求。比如如果浏览器当前处于http://localhost:3001网站下,那么它无法发送请求到http://localhost:3002

下面我们在localhost:3001访问3002中的api。

img

JSONP实现跨域

JSONP是一个比较投机的方式,它利用的是浏览器对script标签的跨域没有限制这一特性来实现发送请求。

<html>
<head>
    <script type="text/javascript" src="jquery.js"></script>
</head>

<body>
    <script>
        // 这里是api的回调
        function handleResult(resp) {
            alert(resp.name);
        }
    </script>
    <!-- 这里访问不同域下的api -->
    <script type="text/javascript" src="http://localhost:3002/controller"></script>
</body>

</html>

而API也不能返回单纯的json了,因为json是纯数据,script标签加载之后并不会产生任何动作。API应该返回包装了回调函数调用的数据,这个函数的参数就是实际JSON数据。如下:

handleResult({
	"name": "app2"
})

这样,当浏览器加载了localhost:3002/controller,它会把响应数据当作js脚本来处理,然后就是调用回调函数handleResult,并把实际的响应传回去。下面是调用成功的一个例子:

img

然而,这用到一个api就要一个script标签,这让人无法忍受,Jquery封装了jsonp的调用:

<html>

<head>
    <script type="text/javascript" src="jquery.js"></script>
</head>

<body>
    <script>
    	$(document).ready(function(){
            $.ajax({
                type : "get",
                async: false,
                url : "http://localhost:3002/controller",
                dataType: "jsonp",
                jsonp:"callback",
                jsonpCallback: "jsonhandle",
                success : function(data) {
                    alert("name:" + data.name);
                }
            });
        });
    </script>
</body>

</html>

这里的jsonp代表请求中参数的名字,jsonCallback代表回调函数的名字,这会在url中组合成localhost:3002?${jsonp}=${jsonpCallback}的形式

Jsonp实现跨域的一个缺点就是,它无法发布post请求,只能发布get请求。

CORS实现跨域

CORS是现代浏览器为了支持跨域请求而造的一个新规范,简单来说就是浏览器会检测到跨域请求,并在其中加一个Origin字段用于告诉服务器请求者当前所在的域。这需要服务器的支持,但是对于前端开发者来说,跨域请求完全由浏览器实现,前端开发者并不需要为之多做什么。

服务器返回后,浏览器会检测服务器返回的头中是否有Access-Control-Allow-Origin字段,如果没有,代表服务器尚未支持跨域,那么不管这次请求是否成功都直接宣告失败。

我们不妨做一个实验,现在,我使用express在3002端口开放两个api:

const express = require('express')
const app = express()
const port = 3002

// 直接返回,响应头中没有任何字段
app.get('/nocors', (req, resp) => {
        res.send("OK");
})

// 响应头中加上Access-Control-Allow-Origin
app.get('/cors', (req, resp) => {
        res.set('Access-Control-Allow-Origin', '*');
        res.send("OK");
})

app.listen(port, () => {
  console.log(`Express app listening on port ${port}`)
})

然后,我在3001端口运行一个服务,并使用ajax分别请求这两个API:

cors.html:

$(document).ready(function () {
    $.ajax({
        type: 'get',
        async: false,
        url: 'http://localhost:3002/cors',
        success: function (data) {
            alert(data);
        }
    });
});

nocors.html:

$(document).ready(function () {
    $.ajax({
        type: 'get',
        async: false,
        url: 'http://localhost:3002/nocors',
        success: function (data) {
            alert(data);
        }
    });
});

cors.html得到了如下响应:

img

nocors.html并未得到响应,控制台打印了如下数据:

img

并且网络界面显示请求失败,并且显示无法加载响应数据,尽管服务器端返回的状态码是200。

img

img

我们用脚丫子想想都知道了,服务器端的nocors逻辑已经被执行,并且返回数据也发回了客户端,只是浏览器没在返回数据中看到Access-Control-Allow-Origin,它就认为请求失败了,但实际上该请求的效果已经产生,如果服务器端的代码有什么副作用,那副作用也已经产生了。

CORS响应头字段

  1. Access-Control-Allow-Origin是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
  2. Access-Control-Allow-Credentials可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
  3. Access-Control-Expose-Headers可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。

关于Cookie的问题,只携带服务器域名设置的Cookie,其它Cookie不会携带。

非简单请求

上面所说的CORS的所有内容都基于简单请求,简单请求的定义如下:

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

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

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

假设现在我们来修改cors.html,来发一个非简单请求:

$(document).ready(function () {
    $.ajax({
        type: 'get',
        async: false,
        url: 'http://localhost:3002/cors',
        // [+] 添加下面这行,不再是简单请求
        contentType: 'application/json', 
        success: function (data) {
            alert(data);
        }
    });
});

再次请求,我们发现请求也失败了,而且网络中出现了两个到cors的ajax请求,其中的一个是OPTION请求:

img

是这样的,对于复杂请求,浏览器会先发送一个OPTION请求预检服务器是否支持CORS,当前我们的服务器显然还不支持OPTION请求,所以这次跨域请求会失败。

在复杂请求中,浏览器会携带两个头,一个代表它将要以什么方法请求跨域API,一个代表它请求头中携带的多余Header。下面是我们的请求头中的这两个参数:

img

服务器端的任务是校验它是否支持这个域的访问,如果支持,检测它是否支持对应的Header和请求方法,修改服务器端的代码:

app.get('/cors', (req, resp) => {
        resp.set('Access-Control-Allow-Origin', '*');
        resp.send("OK");
})
app.options('/cors', (req, resp) => {
        resp.set('Access-Control-Allow-Origin', '*');
        // 告诉浏览器,GET方法被支持
        resp.set('Access-Control-Allow-Methods', 'GET')
        // 告诉浏览器Content-Type头被支持
        resp.set('Access-Control-Allow-Headers', 'Content-Type')
        resp.send("OK");
})

再次请求

img

如果服务器端返回的头中没有后面那两行,浏览器也不会发起真正的请求,它会认为服务器表明它不支持我的请求。

WebSocket

WebSocket是HTML5中新增的,给浏览器与服务器进行套接字通信的接口,它是允许跨域的。

代理

代理就是,比如你通过域A访问域B,那么在域A中架设一台代理服务器,把请求转发到域B,然后浏览器访问域A即可,这其中不涉及到跨域访问了。

前端后端分离开发时经常会用到这个特性,在本地架设一个代理服务器,以免浏览器无法跨域访问api。

其它跨域方式

  • window.postMessage()
  • window.name+iframe
  • 修改document.domain跨子域

参考

  1. Java 最常见的 208 道面试题:第八模块答案
  2. 跨域资源共享 CORS 详解 - 阮一峰的网络日志
posted @ 2022-06-23 18:50  yudoge  阅读(144)  评论(0编辑  收藏  举报