什么是跨域?
在了解跨域之前,我们需要了解浏览器一个非常重要的安全策略——同源策略(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















