关于跨域问题详解
前言
跨域,是Web开发中经常遇到而且在面试中也经常考查的问题,而跨域是什么,跨域的原因又是什么,我们要如何去解决跨域问题?
什么是跨域
跨域,其实是浏览器对JavaScript的一种限制,它限制不同域下的不同Js不能调用彼此。因为它判断此段请求,是不同域的,简而言之,一个域的网站去请求另一个域的资源,这段请求即是跨域的。而浏览器的判断规则,即同源策略。
什么是同源策略
同源策略即请求的地址域名,必须与当前页面的域名相同,包括协议和端口号。只要有其中一个不同,则浏览器会判断该请求跨域。同源策略是浏览器的行为,是为了保护本地数据不被JavaScript代码获取回来的数据污染,因此拦截的是客户端发出的请求回来的数据接收,即请求发送了,服务器响应了,但是无法被浏览器接收。
同源策略限制了:
Cookie、LocalStorage等存储型内容
Dom节点
Ajax请求(非同源)
注意: 标签则是允许跨域加载资源的。
如何解决跨域问题,跨域的方法?
在现实的开发中,我们经常需要进行跨域请求,那么跨域的方法又有什么呢?
以下列出三种跨域方式,可以适用于大部分的开发场景。
-
JSONP
JSONP是一种常见的且兼容性较好的前端跨域方式,在大部分浏览器上都可以实现跨域。唯一的缺点是它只支持Get请求,从而它的传输即是不安全的,容易遭受XSS攻击。
JSONP主要使用了允许跨域的标签来进行跨域。它通过在标签中src属性引入对应的接口链接从而实现跨域请求。
这里我借用掘金@浪里行舟 九种跨域方式实现原理中的JSONP实现代码展示:
// index.html function jsonp({ url, params, callback }) { return new Promise((resolve, reject) => { let script = document.createElement('script') window[callback] = function(data) { resolve(data) document.body.removeChild(script) } params = { ...params, callback } // wd=b&callback=show let arrs = [] for (let key in params) { arrs.push(`${key}=${params[key]}`) } script.src = `${url}?${arrs.join('&')}` document.body.appendChild(script) }) } jsonp({ url: 'http://localhost:3000/say', params: { wd: 'Iloveyou' }, callback: 'show' }).then(data => { console.log(data) })
我们可以看到,传入的url:http://localhost:3000/say 会走到函数中的script标签的src属性中,从而调用document.body.appendChild(script) 去跨域请求。通过JSONP实现了跨域。
同样,如果你不想自己实现jsonp可以引用另一种方式,通过npm install Jsonp ,
在页面中引用,具体代码如下:
jsonp('http://localhost:3000/jsonp', null, (err, data) => { if (err) { console.error(err.message); } else { console.log(data) } });
以及第三种方式,jQuery自带的jsonp方式,具体代码如下:
$.ajax({ url: "http://localhost:9090/student", type: "GET", //jsonp只支持get请求 dataType: "jsonp", //指定服务器返回的数据类型 jsonp: "theFunction", //指定参数名称,默认callback jsonpCallback: "showData", //指定回调函数名称 success: function (data) { var result = JSON.stringify(data); //json对象转成字符串 $("#text").val(result); } });
-
CORS
CORS 指的是跨域资源共享,它需要双方的支持,基础是后端同学在服务端先支持跨域。那么具体是个怎样支持的过程呢?下面展示在Koa下服务端支持跨域的方式。
const router = require('koa-router')(); const send = require('koa-send'); router .all('/*', async (ctx, next)=> { // 设置 Access-Control-Allow-Origin 支持跨域 * 标识支持全部ip跨域,此处也可以使用 ip 地址 ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Content-Type', 'application/json;charset=utf-8'); ctx.set('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS'); ctx.set('Access-Control-Allow-Headers', 'content-type'); ctx.set('Access-Control-Allow-Credentials', 'true'); // 遇到options请求返回200 if (ctx.request.method === 'OPTIONS') { ctx.status = 200; } else { await next(); } }) .get('/', (ctx, next) => { ctx.body = {data: 'get request from /!'}; });
通过设置 Access-Control-Allow-Origin 支持跨域,客户端可以不做任何处理,既可以实现跨域。开启后服务端接收到的请求将分为两种:简单请求与复杂请求
简单请求
简单请求的标志是请求方法限定为 head post get 方法之间的一种,头请求信息不超过以下几种字段:
Accept Accept-Language Content-Language Last-Event-ID Content-Type: application/x-www-form-urlencoded、 multipart/form-data、text/plain
则这类请求称之为简单请求。
复杂请求
复杂请求的标识是,浏览器会代理你发出一个option请求进行校验,他们头请求信息最常见的就是content-type=applicaiton/json,传参的方式自然就是Json串,下面展示一次复杂请求:
$.ajax( { url: 'http://127.0.0.1:3000/post', type: 'post', data: { a: 1 }, dataType: 'json', contentType: 'applicaiton/json', success: function(res) { console.log(res); }, complete: function(xhr, ts) { console.log('Status Code', xhr.status); } } );
而OPTIONS请求一般会带三个关键请求头:
Access-Control-Allow-Headers Access-Control-Allow-Methods Access-Control-Allow-Origin
当校验成功的时候,就会发送真正的请求了。
通过CORS进行跨域是一个极好的解决方案,在服务端开启跨域之后,客户端可以不做任何处理,浏览器会自行进行处理,对于请求方是无感知的(options请求)。
Tips: 正常情况下,服务端开启跨域支持后,如果你的的浏览器依旧显示不能跨域,可尝试使用浏览器端的CORS插件再进行尝试。
-
WebSocket
WebSocket 是HTML5的一个基于TCP的持久化协议(HTTP也是基于TCP的),它实现了浏览器的全双工通信,它的具体工作流程是这样的,通过HTTP与服务端建立连接,建立连接之后,服务端与客户端之间既可以实现双向信息传输,客户端可以主动向服务端发送信息,服务端也可以主动跟客户端交互,而这之间的交互就是基于WebSocket的。以下举个koa中实现WebScoket的例子,附代码。
// 服务端代码 const Koa = require('koa'); const app = new Koa(); // 挂载中间件可以在这里挂载 // ... // websocket 将已挂载中间件的 app 打入 server const server = require('http').createServer(app.callback()); const io = require('socket.io')(server); server.listen(3000, () => { console.log(`app run at : http://127.0.0.1:3000`); }) // websocket 开启监听 io.on('connection', socket => { socket.on('send', data => { console.log('客户端发送的内容:', data); socket.emit('getMsg', '返回消息: 我接收到了' + data); }); console.log('websocket初始化成功'); socket.emit('getMsg', 'websocket初始化成功...'); });
// 客户端代码 通过http-server开启一个服务来打开页面 <!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"> <meta http-equiv="Cache-Control" content="no-cache"> <!-- 此处的地址为服务端地址下的socket文件,我的服务器为127.0.0.1:3000 --> <script src="http://127.0.0.1:3000/socket.io/socket.io.js" ></script> <title>Document</title> </head> <body> <button id="send" onclick="emitFun()">发送到服务器</button> </body> <script> var socket = io('ws://127.0.0.1:3000'); var send = document.querySelector('#send'); var msg = document.querySelector('#msg'); socket.on('getMsg', data => { console.log('服务端消息:', data); }) function emitFun() { console.log('点击了发送消息!'); // 通过emit触发发送请求 socket.emit('send', 'hello'); } </script> </html>
效果:
跨域的方式有很多种,以上只展示常见的三种,也是我自己比较熟悉的三种跨域方式,跨域在我们的前后端分离的大环境下,已经变成家常便饭一样的存在,不能好好处理跨域问题的程序员不是一个好程序员。
参考文献/博客
- 跨域的几种方式 http://www.imooc.com/article/40074
- 九种跨域方式实现原理 https://juejin.im/post/5c23993de51d457b8c1f4ee1
- socket.io 官网 https://socket.io/