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