什么是跨域?

在了解跨域之前,我们需要了解浏览器一个非常重要的安全策略——同源策略(Same Origin Policy)。

所谓同源策略,是指浏览器只允许 JavaScript 读取相同【协议、域名、端口】下的数据以保证用户的信息安全,防止恶意网站窃取数据。其限制的内容主要包括:Cookie、LocalStorage 和 IndexDB 中数据的读取;AJAX 请求的发送;DOM 元素的获取等。该安全策略 1995 年由网景公司(Netscape)提出。

跨域即非同源策略请求,是想要访问不同【协议、域名、端口】下的数据。就好像显示生活中你想要的获取别人家银行卡密码,这显然是不应当被允许的。所以说,应当尽可能地避免允许跨域请求。

为什么要跨域?

虽然我们希望尽可能地避免跨域请求,但是面对一些实际的场景又不得不跨域请求。比如:

  • 前后端分离开发,开发过程中后端与前端网络地址不同
  • 实际生产环境中,有必要将前后端分别部署到多个服务器上(大型项目考虑安全及性能问题可能会将数据服务器、网站服务器、文件服务器分离)
  • 项目中需要请求一些第三方 API 服务,比如一些图文识别、语音识别、人脸识别、天气预报、在线支付等

如何跨域?

常见的跨域资源包括 JSONP 或者 CORS 等,下面分别简单说明一下。
注意:所有的跨域都需要在服务器端做一些设置

1.JSONP 跨域

一些带 src 或 href 属性的 HTML 标签可以不受同源策略限制,比如:script、img、link、iframe 等。JSONP 即是通过动态创建 script 标签,引入外部 JavaScript 文件实现跨域的。与其说是一种技术,JSONP 更像是一个 BUG。 值得注意的是 JSONP 仅支持 GET 请求。
原生 JavaScript 实现JSONP

服务器代码
使用基于 nodejs 的 express 搭建的服务器,注意 handle 处理函数

// 原生 JS 发起 JSONP
app.get('/jsonp-server', (request, response)=>{
    // 原始数据
    const data = {
        name: '蝈蝈要安静',
        url: 'https://blog.quietguoguo.com'
    }
    let json_str = JSON.stringify(data)
    // 设置响应内容
    response.send(`handle(${json_str})`);
})

客户端代码

const btns = document.getElementsByTagName('button');
const result = document.getElementsByClassName('result')[0];

/** 原始 JavaScript 实现 JSONP */
// 1.声明 handle 函数 —— 服务器端定义的处理函数名
function handle(data){
    console.log(data)
    result.innerText = data.name;
}
// 按钮点击事件
btns[0].onclick = function(){
    // 2.创建 script 标签
    const scriptTag = document.createElement('script');
    // 3.设置 script 标签的属性
    scriptTag.src = "http://127.0.0.1:3000/jsonp-server";
    scriptTag.id = "js-jsonp";
    // 4.添加 script 标签到文档中
    document.body.appendChild(scriptTag);
    // 5.移除 script 标签,保持 DOM 整洁
    document.body.removeChild(scriptTag);
}

jQuery 实现JSONP
上面原生方法中需要服务器提供一个具体名称的处理函数(比如这里的 handle),这样不可避免的会增大开发过程中前后端的沟通成本,为此我们考虑可以由前端传递一个函数名给后端,后端根据传递的函数名动态生成处理函数名会更方便。jQuery 中就封装了这么一个方法实现 JSONP 请求。
jQuery 通过 JSONP 发起跨域请求的时候,需要在请求地址后带上 ?callback=functionName 这么一个参数,functionName 为告诉后端前端处理数据的函数,后端动态创建此函数名为处理函数即可。

服务端代码
使用基于 nodejs 的 express 搭建的服务器,注意动态生成的处理函数

// jQuery 发起 JSONP
app.get('/jquery-server', (request, response)=>{
    // 原始数据
    const data = {
        name: 'JQuery 发送 JSONP',
        site: '蝈蝈要安静',
        url: 'https://blog.quietguoguo.com'
    }
    let json_str = JSON.stringify(data);
    // 获取处理函数名
    let handle = request.query.callback;    // 前端传递过来的处理函数名
    // 设置响应内容
    response.send( `${handle}(${json_str})` );
})

客户端代码

/** jQuery 实现跨域 */
$('button').eq(1).click(() => {
    $.ajax({
        url: 'http://127.0.0.1:3000/jquery-server',
        method: 'GET',
        dataType: 'jsonp',    //  当前发起的是一个 JSONP 请求
        jsonpCallback: 'callbackName',    // 指定处理函数名,可选
        success: (data) => {
            console.log(data)
            $('.result').eq(0).html(data.name)
        }
    })
})

2.CORS 跨域

跨源资源共享 (Cross Origin Resource Sharing) 是一种基于 HTTP 头的机制,它允许服务器标示除了同源请求以外的其它请求。相比于 JSONP 只能使用 GET 请求,CORS 可以自定义允许的请求。CORS 的配置项包括:

  • Access-Control-Allow-Origin :设置允许通过的请求源【协议、域名、端口】(只能写一个源,不太好用)
  • Access-Control-Allow-Methods :设置允许的请求方式【GET、POST、OPTION、HEAD、PUT、DELETE、TRACE、CONNECT】
  • Access-Control-Allow-Credentials : 设置是否允许证书验证【true 或 false】
  • Access-Control-Max-Age :设置允许预检请求的最大超时时间,单位:秒(s)
  • Access-Control-Allow-Headers :设置允许的请求头信息【Content-Type,Authorization…】
  • Access-Control-Expose-Headers : 设置允许公开的请求头信息【Content-Type,Authorization…】
  • Access-Control-Request-Headers :设置请求预检时让服务器知道哪些头信息将在正式请求时被使用【Content-Type,Authorization…】
  • Access-Control-Request-Methods :设置请求预检时让服务器知道哪些方法将在正式请求时被使用【GET、POST、OPTION、HEAD、PUT、DELETE、TRACE、CONNECT】

服务端代码
使用基于 nodejs 的 express 搭建的服务器,注意 CORS 相关设置

// CORS 发起 JSONP
app.all('/cors-server', (request, response)=>{
    // 原始数据
    const data = {
        name: 'CORS 允许跨域请求',
        site: '蝈蝈要安静',
        url: 'https://blog.quietguoguo.com'
    };
    // CORS 相关设置
    response.setHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5500");    // 允许 http://127.0.0.1:5500 的跨域请求,该地址为我用 VSCode 的 Live Server 临时生成的
    response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type");   // 允许预检的请求头信息
    response.setHeader('Access-Control-Allow-Methods', 'GET, POST');    // 允许的请求方式
    response.setHeader('Access-Control-Allow-Credentials', true);      // 允许证书验证
    // 发送响应内容
    response.send( data );
})

客户端代码

/** CORS 实现跨域 */
btns[2].onclick = function(){
    const xhr =new XMLHttpRequest();
    xhr.open('GET', 'http://127.0.0.1:3000/cors-server')
    xhr.send();
    xhr.onreadystatechange = function(){
        if(xhr.readyState === 4){
            if(xhr.status >= 200 && xhr.status < 300){
                const data =JSON.parse( xhr.response )
                console.log( data )
                result.innerText = data.name;
            }
        }
    }
}

前端代码与正常请求代码一样即可。

3.HTTP 代理服务器跨域

服务器与服务器之间不存在跨域问题。通过设置本地服务器去访问目标服务器,然后本地服务器返回数据就不存在跨域问题。WebpPack 中提供 devServer 参数用于配置开发服务器,其中有个 proxy 配置项可用于配置代理服务器,实现开发阶段的跨域需求。

webpack.config.js 配置
注意,这里 WebPack 会自动将 /proxy-server 下的请求信息,自动代理到 http://127.0.0.1:3000 源下。更多配置可参考WebPack devServer

// 配置代理服务器解决跨域问题
devServer: {
    port: 3001,
    proxy: {
        '/proxy-server': {
            target: 'http://127.0.0.1:3000/',
            changeOrigin: true
        }
    },
}

请求代码

const btns = document.getElementsByTagName('button');
const result = document.getElementsByClassName('result')[0];
/** WebPack 代理服务器解决跨域 */
btns[0].onclick = function(){
    const xhr =new XMLHttpRequest();
    xhr.open('GET', '/proxy-server')
    xhr.send();
    xhr.onreadystatechange = function(){
        if(xhr.readyState === 4){
            if(xhr.status >= 200 && xhr.status < 300){
                const data = JSON.parse( xhr.response )
                console.log( data )
                result.innerText = data.name;
            }
        }
    }
}

请求时正常请求即可,不过需要注意的是,这只适合开发阶段的跨域需求。

4.Nginx 反向代理

使用 Nginx 反向代理将 www.a.com/apis/ 的请求代理到 www.b.com 的源下

# 代理服务器
server {
    listen       80;
    server_name  www.a.com;
    # 匹配以/apis/开头的请求
    location ^~ /apis/ {
        proxy_pass http://www.b.com/;  #注意域名后有一个/
        add_header Access-Control-Allow-Origin www.b.com;
    }
}

5.iframe + postMessage 跨域

该方法使用 iframe 标签及 postMessage 接口实现了 Window 对象之间的跨域通信。

B页面(服务端)核心代码

// 监听A页面发来的消息
window.onmessage = function(event) {
    console.log(event)
    event.source.postMessage("B页面(服务端)返回给A页面(客户端)的消息", event.origin)
}

A页面(客户端)核心代码

const btns = document.getElementsByTagName('button');
const result = document.getElementsByClassName('result')[0];

btns[0].onclick = function(){
    const iframeA =document.getElementById("iframeA")
    let targetOrigin = 'http://127.0.0.1:3003';
    iframeA.contentWindow.postMessage('A页面(客户端)向B页面(服务端)发送消息', targetOrigin);
    // 监听B页面发来的消息
    window.onmessage = function(event) {
        console.log(event)
        result.innerHTML = event.data
    }
}

更多 postMessage 接口信息参考:postMessage
其他通过 iframe 标签实现跨域的方式有:

  • iframe + document.domain 跨域
  • iframe + window.name 跨域
  • iframe + location.hash 跨域

各有优缺点,基本上不会用到,此处略过。

6.WebSocket 跨域

WebSocket 是基于 TCP 的一种新的网络通信协议,它实现了服务器与浏览器间的全双工通信(允许服务器主动发送信息给客户端)。从 HTML5 开始,RFC6455 定义了它的通信标准。WebSocket 对象作为一个构造函数,在使用前需要将其实例化:

var Socket = new WebSocket(url, [protocol] );
  • url :必填,请求的地址
  • protocol :可选,可接受的子协议

属性

  • readyState :只读,连接状态(0-未连接;1-已连接,可通信;2-正在关闭;3-已经关闭)
  • bufferedAmount :只读,已被 send 放入队列中等待传输,但还没有发出的 UTF-8 文本字节数

事件

  • onopen :连接建立时触发
  • onmessage :客户端接收服务端数据时触发
  • onerror :通信发生错误时触发
  • onclose :连接关闭时触发

方法

  • send() :使用连接发送数据
  • close() :关闭连接

服务器代码
这里服务器端使用的 express-ws 建立的 WebSocket 链接

// WebSocket 
app.ws('/websocket-server', function(ws, request) {
    console.log('WebSocket 已连接');
    // 原始数据
    const data = {
        name: 'WebSocket 发送跨域消息',
        site: '蝈蝈要安静',
        url: 'https://blog.quietguoguo.com'
    };
    let json_str = JSON.stringify(data);
    ws.on('message', function(msg) {
        ws.send(json_str);
    });
});

客户端代码

const result = document.getElementsByClassName('result')[0];
const btns = document.getElementsByTagName('button');
const ipt = document.getElementById('input')

var ws = new WebSocket('ws://127.0.0.1:3000/websocket-server');

ws.onopen = function (event) {
    console.log(event)
    console.log('WebSocket 服务已连接')	
};

ws.onmessage = function (event) {
    if(ws.readyState === 1){
        console.log(event)
        console.log('客户端已接收服务器数据')
        let msg = JSON.parse(event.data);
        console.log(msg)
        result.innerHTML = msg.name
    }
};

ws.onerror = function (event) {
    console.log(event)
    console.log('通信发生错误')
};

ws.onclose = function (event) {
    console.log(event)
    console.log('通信已关闭')
};

/** WebSocket 实现跨域 */
btns[0].onclick = function(){
    ws.send(input.value)
}
btns[1].onclick = function(){
    ws.close()
}

更多 WebSocket 内容参考:WebSocket