内容简介:笔者经常在前端开源群答疑,加上之前的招聘面试经历。发现许多新手前端在问起跨域问题的解决方案,一套一套的,可是实际遇到跨域问题了就不知道怎么解决了。这次写这篇文章从实践角度聊一聊跨域问题。这句话的三个关键字:
笔者经常在前端开源群答疑,加上之前的招聘面试经历。发现许多新手前端在问起跨域问题的解决方案,一套一套的,可是实际遇到跨域问题了就不知道怎么解决了。这次写这篇文章从实践角度聊一聊跨域问题。
跨域基本概念
出于浏览器的同源策略限制,浏览器会拒绝跨域请求。 这就是跨域问题的产生原因,同源策略是用于隔离潜在恶意文件的重要安全机制。
这句话的三个关键字:
- 同源
- 限制
- 浏览器拒绝
什么是同源
那么第一个问题来了,什么算是同源。解决这个问题需要先了解一下URL的完整结构:
两个URL的才算同源。反而言之,三者任何一个不相同都算跨域。 例如某个页面地址为 www.domain.com/page1.html, 该页面访问以下API接口跨域关系表:
API 接口地址 | 是否跨域 | 原因 |
---|---|---|
www.domain.com/api/users/1 | 否 | 协议、主机、端口全部都相同 |
www.domain.com:80/api/users/1 | 是 | 端口不同 |
www.baidu.com/api/users/1 | 是 | 协议不同 |
api.domain.com/v1/users/1 | 是 | 主机不同 |
有哪些限制
- XmlHttpRequest(即ajax请求)和Fetch两种接口发出的HTTP请求进行限制。
- 对于嵌入资源标签
script
、img
、link
、video
等标签加载资源的请求(HTTP GET请求)不做限制。
具体的限制规则还有很多,这里只说常见和本文用得上的。
浏览器拒绝
那么那些环境算是浏览器?
- PC端常见的 Chrome/Safari/Edge
- 移动端的Chrome/Safari/各个App内嵌Webview 浏览器又是怎么拒绝的 先来看一张图,一个用户点击了一个按钮,发出了一个AJAX GET请求。那么常见的流程如图:
那么如果用户发出的AJAX GET请求是一个跨域请求,那么会在上图中哪一个阶段被阻止? 但是第3阶段,也就是说用户发送的信息可以到达服务端,服务器是能够接受处理并返回了。返回的浏览器发现这是一个跨域请求。就直接拒绝,同时把返回的信息替换为报错信息,返回给JavaScript程序。 对于更复杂的POST/PUT等请求,MDN CORS文档里面有更详细的处理方法。这里就不细说。
这一点很重要,但是总是被新人忽略。所以重要的事情说三遍,
- 拒绝跨域请求是浏览器
- 拒绝跨域请求是浏览器
- 拒绝跨域请求是浏览器
反过来说,Nginx、Java/Nodejs等编程语言的HTTPClient以及手机App,他们发出的HTTP请求就完全没有跨域问题,因为他们不是浏览器,没有实现W3C规范。
跨域解决方案
JSONP
在浏览器中假设有以下一段代码会执行结果会是什么样?
<script> window.callback = function (data) { console.log(data); delete window.callback; } </script> <script> callback({ "code": 1, "data": [1,2,3] }); </script> 复制代码
毫无疑问,肯定是在控制台输出了一个对象信息。 记得刚才在介绍跨域基本概念的时候说个浏览器不限制 script
标签加载js文件。那么把这二者的特性相结合。第二个 script
标签改为从网络加载. 就可以实现跨域. 例如 一个跨域API http://api.domain.com/v1/users/1
<script src="http://api.domain.com/v1/users/1?callback=callbackFun"></script> GET http://api.domain.com/v1/users/1?callback=callbackFun application/javascript
callbackFun({/*需要的数据*/}); 复制代码
- 数据返回成功以后处理数据,删除script标签
以上步骤就是JSONP的思想。实现一个完善的JSNOP请求库还有细节要处理,比如超时取消、回调函数防重名等。很多开源库(jQuery, axios)都实现了JSNOP请求。想要代码的去Github阅读源码,这里就不给出代码。
优劣势
JSONP虽然是一种实现跨域访问的方法,前端想要使用JSONP进行跨域访问却不容易。
GET http://api.domain.com/v1/users/1
ContentType: application/json 复制代码
{ "code": 1, "data" : {"userid": 1} } 复制代码
而 GET http://api.domain.com/v1/users/1?callback=callbackFun
返回
ContentType: application/javascript 复制代码
callbackFun({ "code": 1, "data" : {"userid": 1} }); 复制代码
既然可以和后端商量配合你改造接口,那还有更好的方案可以解决。何必用这种方案。
JSONP有一个有点就是兼容性好,IE678通通兼容,所以一般JSNOP是后端同学如果主动需要开放API给他人使用,同时有需要极高的兼容的一个妥协方案。一般情况下不推荐这个方案。
JSONP 开心一刻
真实经历。之前开发项目需要调用另一个项目组的接口。 跨域造成接口掉不通,然后找Z君沟通, Z君说:"你用JSONP来掉接口就好了。这都不知道...." 然后我还在想大神这么NB的么,JSONP兼容都提前做完了。我试了JSONP。坑爹呀,你后端根本就没兼容JSONP,我怎么调用,呵呵... 呵呵呵呵....
请求代理
JSONP方案不推荐,那么又需要访问跨域接口,怎么办呢? 重要事情不在乎再多说一遍 拒绝跨域请求是浏览器 。 那么如果有一个非W3C标准的HttpClient帮助我们转发请求,不就可以了实现跨域访问了。
App端
通常App对于webview都有很强的控制权,可以在Webview的JS环境中注入一些方法。 那么移动端 程序员 可以在Webview中注入一个接口,运行在里面的js代码可以通过这个方法把自己的请求地址、请求参数、请求体等数据交给App Native端,让App Native代为收发请求。App Native不是浏览器,不受跨域限制。
具体实现方法可以搜 Hybird App开发或者请教移动端开发的同学。
Web端
Web端必然运行在浏览器环境中,那么没有App Native。还有服务器上可以做反向代理。 所谓的反向代理,原理和App Native请求代理的原理差不多,就是我们请求非跨域下的反向代理服务,反向代理服务会把你的请求转发给目标服务器。 反向代理服务可以是Nginx也可以是java/Nodejs程序等等。这些程序也不受跨域限制,可以接受目标服务器的请求,并返回给我们。
React/Vue 开发阶段跨域处理
React/Vue 这种SPA开发施行的完全的前后端分离的模式,开发阶段必然是需要跨域访问接口的。 Vue开发可以这样配置:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: '<url>', ws: true, changeOrigin: true }, '/foo': { target: '<other_url>' } } } } 复制代码
详情见Vue CLI文档。React也有类似的配置,详情见 create-react-app文档
那么他们具体是怎么实现的呢? 本地起一个服务端程序提供反向代理的能力。而React/Vue本地启动的这个服务端程序就是Webpack-dev-server。来探索 Webpack-dev-server源码 ,源码中启动server的关键代码在 lib/Server.js 中,挑重点
/* 此处省略许多行代码 */ // 27行 引入express 作为服务端框架 const express = require('express'); /* 此处省略许多行代码 */ // 31行 引入 http-proxy-middleware 提供反向代理的能力 const httpProxyMiddleware = require('http-proxy-middleware'); /* 此处省略许多行代码 */ // 328行 获取 proxyMiddleware 并加载到为express的中间件Middleware app.use((req, res, next) = > { if (typeof proxyConfigOrCallback === 'function') { const newProxyConfig = proxyConfigOrCallback(); if (newProxyConfig !== proxyConfig) { proxyConfig = newProxyConfig; // 334行 根据 proxyConfig 获取 处理proxy请求的中间件proxyMiddleware proxyMiddleware = getProxyMiddleware(proxyConfig); } } const bypass = typeof proxyConfig.bypass === 'function'; const bypassUrl = (bypass && proxyConfig.bypass(req, res, proxyConfig)) || false; if (bypassUrl) { req.url = bypassUrl; next(); } else if (proxyMiddleware) { // 347行 最最关键一行 经过多次判定某个请求是需要代理转发的请求,那么把它交给proxyMiddleware进行处理, proxyMiddleware return proxyMiddleware(req, res, next); } else { next(); } }); 复制代码
以上代码有点NodeJS服务端开发的同学基本能看明白,看不明白也没关系。你知道React/Vue可以通过相应的配置项获得接口跨域访问的能力就可以了。其中最核心的就是依靠Express的网络请求能力充当反向代理服务器。
React/Vue 线上部署阶段跨域处理
开发阶段还可以通过本地启动一个Express服务器作为代理,帮助我们处理跨域问题,问题是生产环境是不推荐这么做的。React/Vue 项目通常在build以后会生成以下文件:
- xxx.html 文件1份
- xxx.xxxxxx.js Javacript文件若干
- xx.xxxx.css 文件若干
- xxx.map 文件若干,当然也可能没有 而且里面的js/css/图片等文件通常部署在cdn上,最为要紧的页面入口 index.html 则需要小心部署,否则易遇到2个问题
- 页面没办法访问
- 接口跨域导致没办法访问
对于index.html的部署,Vue-Router文档写的很清楚。推荐通过nginx try-file命令来进行部署。同时nginx又是一个反向代理服务器。假设 网页需要在host http://www.domain.com/
下, 真实API服务部署在 http://api.domain.com/api
。那么通过反向代理把接口代理到 http://www.domain.com/api
下。那么跨域访问就变成了同域名访问。 那么nginx的配置文件可以这样写
server { listen 80; server_name www.domain.com ; root www; # 存放html文件的文件夹 location ^/api { # 接口代理到 8080 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_pass http://api.domain.com/api; } location / { # 其他请求返回index.html try_files $uri $uri/ /index.html; } } 复制代码
这样做完, 访问API就会被代理转发,访问其他路径就返回html。如下图所示:
线上部署的方式可能根据系统架构选型而多种多样。这只是其中一种比较通用且为官方推荐的方式。仅做参考。类似ngixn的服务端软件还是Caddy、Envoy
这种方案的优点是不需要后端同学改动接口,只需要运维小哥帮助配置一下nginx即可完成兼容。缺点是多一次转发可能带来性能损失。
CORS
实际情况多种多样,有些时候没办法使用JSONP,也通过nginx转发又会产生性能损失。那么还有一个终极大招———— CORS.
W3C的同源策略出来以后造成了很多不便,无法应对某些跨域访问的强需求。为此W3C增加了CORS相关的规范, 文档之前也提及过:MDN CORS文档。
重要的事情再重复一遍: 拒绝跨域请求是浏览器 ,那么CORS的原理就是CORS相关的规范中制定了一些响应头(Response Header),这些响应头以 Access-Control-Request-
开头。简单枚举几个,具体这些头的含义和用法见MDN CORS文档.
Access-Control-Request-Method: POST Access-Control-Request-Headers: X-PINGOTHER, Content-Type Access-Control-Allow-Origin: http://foo.example Access-Control-Allow-Methods: POST, GET, OPTIONS Access-Control-Allow-Headers: X-PINGOTHER, Content-Type Access-Control-Max-Age: 86400 复制代码
浏览器在接收到设置过CORS响应头的返回以后,会根据CORS规范检查合法性,检查通过则不再阻止,放行通过。
简而言之就是CORS响应头就是用来告诉浏览器:"我是虽然是跨域请求,但是我是合法的,请不要拒绝我"。
CORS方案的优点是支持各种方法 GET、POST、PUT、DELTE等等。而且改动量比较小。可以在服务端程序比如 Java 或者NodeJS上做,也可通过前置代理服务器nginx完成。
缺点就是
- 浏览器兼容性差
- 降低了安全性,毕竟W3C之所以禁止跨域,是为了安全。现在推出CORS方案虽然已经在安全和灵活方面做到一个较好平衡。但是如果CORS响应头设置不当,还是可能会产生安全问题。
其他
其他还有用与父页面与子页面(iframe)之间的通信的跨域问题,window.name、postMessage等方法。这里就不详细说了。日常用的确实不多,有需要再查把。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- 详解RunLoop之面试题
- 前端面试题—vue部分详解
- 【Android面试】HashMap详解(一)
- 2019 JavaScript面试题详解(基础+进阶)
- iOS 模式详解—「runtime 面试、工作」看我就 ???? 了 ^_^.
- 数据结构和算法面试题系列—二分查找算法详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
C++沉思录
Andrew Koenig、Barbara Moo / 黄晓春、孟岩(审校) / 人民邮电出版社 / 2002-11-01 / 50.00元
《C++ 沉思录》集中反映了C++的关键思想和编程技术,不仅告诉你如何编程,还告诉你为什么要这样编程。本书曾出现在众多的C++专家推荐书目中。 这将是C++程序员的必读之作。因为: 它包含了丰富的C++思想和技术,从详细的代码实例总结出程序设计的原则和方法。 不仅教你如何遵循规则,还教你如何思考C++编程。 既包括面向对象编程也包括泛型编程。 探究STL这一近年来C++最重要的新成果的内在思想。一起来看看 《C++沉思录》 这本书的介绍吧!