原文链接:https://github.com/FIGHTING-TOP/FE-knowlodge-base
什么是跨域请求
当前发起请求的域与该请求指向的资源(该请求所在的页面)所在的域不一样。
此时该请求就会受到浏览器SOP(同源策略)的限制,这样做可以降低被CSRF(跨站请求伪造)攻击的可能,但是同源策略并不能避免CSRF。对CSRF感兴趣的可以戳这里
这里的域指的是这样的一个概念:我们认为若协议 + 域名 + 端口号均相同,那么就是同域。
如何解决ajax跨域
- JSONP
- CORS
- 代理请求
- 图片Ping
- websocket
(一)JSONP方式解决跨域问题
jsonp解决跨域问题是一个比较古老的方案(实际中不推荐使用),这里做简单介绍(实际项目中如果要使用JSONP,一般会使用JQ等对JSONP进行了封装的类库来进行ajax请求)
实现原理
JSONP之所以能够用来解决跨域方案,主要是因为 <script> 脚本拥有跨域能力,而JSONP正是利用这一点来实现。
当我们用一个script的src来请求一个接口时,这个接口返回了一个函数名和一个或一些参数,由于是script引入的,所以浏览器就直接给我们执行了,注意在这之前我们必须先注册好接口返回的方法。
实现流程
JSONP的实现步骤大致如下(参考了来源中的文章)
-
客户端网页网页通过添加一个<script>元素,向服务器请求JSON数据,这种做法不受同源政策限制
function addScriptTag(src) { var script = document.createElement('script'); script.setAttribute("type","text/javascript"); script.src = src; document.body.appendChild(script); } window.onload = function () { addScriptTag('http://example.com/ip?callback=foo'); } function foo(data) { console.log('response data: ' + JSON.stringify(data)); };
请求时,接口地址是作为构建出的脚本标签的src的,这样,当脚本标签构建出来时,最终的src是接口返回的内容
- 服务端对应的接口在返回参数外面添加函数包裹层
foo({ "test": "testData" });
- 由于<script>元素请求的脚本,直接作为代码运行。这时,只要浏览器定义了foo函数,该函数就会立即调用。作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse的步骤。
注意,一般的JSONP接口和普通接口返回数据是有区别的,所以接口如果要做JSONP兼容,需要进行判断是否有对应callback关键字参数,如果有则是JSONP请求,返回JSONP数据,否则返回普通数据
使用注意
基于JSONP的实现原理,所以JSONP只能是“GET”请求,不能进行较为复杂的POST和其它请求,所以遇到那种情况,就得参考下面的CORS解决跨域了
(二)CORS解决跨域问题
CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。
同源安全策略 默认阻止“跨域”获取资源。但是 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。
他允许我们设置一些请求头信息来解决跨域:
Access-Control-Allow-Origin // 指示请求的资源能共享给哪些域。 Access-Control-Allow-Credentials // 指示当请求的凭证标记为 true 时,是否响应该请求。 Access-Control-Allow-Headers // 用在对预请求的响应中,指示实际的请求中可以使用哪些 HTTP 头。 Access-Control-Allow-Methods // 指定对预请求的响应中,哪些 HTTP 方法允许访问请求的资源。 Access-Control-Expose-Headers // 指示哪些 HTTP 头的名称能在响应中列出。 Access-Control-Max-Age // 指示预请求的结果能被缓存多久。 Access-Control-Request-Headers // 用于发起一个预请求,告知服务器正式请求会使用那些 HTTP 头。 Access-Control-Request-Method // 用于发起一个预请求,告知服务器正式请求会使用哪一种 HTTP 请求方法。
Origin // 指示获取资源的请求是从什么域发起的。
另外,规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET
以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST
请求),浏览器必须首先使用 OPTIONS
方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。服务器确认允许之后,才发起实际的 HTTP 请求
在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。
CORS请求失败会产生错误,但是为了安全,在JavaScript代码层面是无法获知到底具体是哪里出了问题。你只能查看浏览器的控制台以得知具体是哪里出现了错误。
然而并非所有的请求都会有预检请求,详细资料可以阅读MDN文档介绍的 跨源资源共享(CORS) 。
(三)代理请求方式解决接口跨域问题
注意,由于接口代理是有代价的,所以这个一般仅是开发过程中使用。
与前面的方法不同,前面CORS是后端解决,而这个主要是前端对接口进行代理,也就是:
- 前端ajax请求的是本地接口
- 本地接口接收到请求后向实际的接口请求数据,然后再将信息返回给前端
- 一般用node.js即可代理
关于如何实现代理,方法有很多,那么我们开发一般会用webpack-dev-server,
假如我们服务在 localhost:3000
上,你可以这样启用代理:
webpack.config.js
module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:3000', pathRewrite: {'^/' : ''} } } } };
请求到 /api/users
现在会被代理到请求 http://localhost:3000/api/users
。
如果你不想始终传递 /api
,则需要重写路径:
webpack.config.js
module.exports = { //... devServer: { proxy: { '/api': { target: 'http://localhost:3000', pathRewrite: {'^/api' : ''} } } } };
这种代理的方式呢,其实是用http-proxy-middleware来实现的, 有兴趣的可以看下源码了解下,有机会可以专门聊下这个
(四)图片Ping
我们知道,一个网页可以从任何网页加载图像,不用担心跨域不跨域,所以,我们就可以利用图片不受“同源限制”这一点进行跨域通信。
我们利用JS创建一个新的Image对象,并把src属性设置为指向请求的地址,通过监听onload和onerror事件来确定是否接受到了响应。响应的数据可以是任意内容,但通常是像素图或304响应。
需要注意的是,新图像元素只要设置了src属性就会开始下载,所以我们这里的事件一定要在指定src属性之前绑定,这也是为什么我们这里不需要把img标签插入DOM 的原因。
<body> <button id="Ping">图像Ping发送请求</button> <script> var btn=document.getElementById('Ping'); btn.onclick=function () { var img=new Image(); img.onload=img.onerror=function () { alert("Done"); }; img.src="http://localhost:8081/img?name=Joy"; } </script> </body> //服务器代码 app.get('/img',function (req,res) { res.send("我是一张图片"); });
这种方式优点是很明显的:兼容性非常好,缺点就是:只能发生GET请求,而且无法获取响应文本。
(五)WebSocket
WebSockets 是一种先进的技术。它可以在用户的浏览器和服务器之间打开交互式通信会话。使用此API,您可以向服务器发送消息并接收事件驱动的响应,而无需通过轮询服务器的方式以获得响应。
它默认是可以跨域的。
// Create WebSocket connection. const socket = new WebSocket('ws://localhost:8080'); // Connection opened socket.addEventListener('open', function (event) { socket.send('Hello Server!'); }); // Listen for messages socket.addEventListener('message', function (event) { console.log('Message from server ', event.data); });
对于前端页面之间的跨域
- 如果是iframe可以使用 location.hash 或 window.name 进行信息交流
- 使用 postMessage
- document.domain(仅限于一级域名和二级域名之间)