jsonp-反向代理-CORS解决JS跨域问题的个人总结(更新 v2.0)
网上说了很多很多,但是看完之后还是很混乱,所以我自己重新总结一下。
解决 js 跨域问题一共有 8 种方法:
- jsonp(只支持 get)
- 反向代理
- CORS
- document.domain + iframe 跨域
- window.name + iframe 跨域
- window.postMessage
- location.hash + iframe
- web sockets
各个方法都有各自的优缺点,但是目前前端开发方面比较常用的是 jsonp,反向代理,CORS:
- CORS 是跨源资源分享(Cross-Origin Resource Sharing)的缩写。它是 W3C 标准,是跨源 AJAX 请求的根本解决方法。
- 优点是:正统,符合标准,
- 缺点是:需要服务器端配合,比较麻烦。
- JSONP 的核心则是动态添加
<script>
标签来调用服务器提供的 js 脚本。- 优点是:对旧式浏览器支持较好,
- 缺点 1: 只支持 get 请求。
- 缺点 2:有安全问题 (请求代码中可能存在安全隐患)。
- 缺点 3:要确定 jsonp 请求是否失败并不容易。
- 反向代理都能够兼容以上的确定,但是仅仅作为前端开发模式的时候使用,在正式上线环境较少用到。
- 一般开发环境的域名跟线上环境不一样才需要这样处理。
- 如果线上环境太复杂,使用反向代理实现跨域的将会变得很麻烦,那么这时候会需要采用 jsonp 或者 CORS 来处理。
这里主要说明这三种方式。其他方式暂不说明。
一、什么是跨域问题
跨域问题一般只出现在前端开发中使用 javascript 进行网络请求的时候,浏览器为了安全访问网络请求的数据而进行的限制。
提示的错误大致如下:
No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://XXXXXX' is therefore not allowed access.
二、为什么会出现跨域问题
因为浏览器受到同源策略的限制,当前域名的 js 只能读取同域下的窗口属性。
换句话来说,就是跨越了浏览器的同源策略限制的时候,就会触发了我们所说的 “跨域” 问题。
2.1 什么是同源策略
同源指的是三个源头同时相同:
- 协议相同
- 域名相同
- 端口相同
举例来说, http://www.example.com/dir/page.html
这个网址,
协议是 http://域名是 www.example.com端口是80 //它的同源情况如下:http://www.example.com/dir2/other.html:同源http://example.com/dir/other.html:不同源(域名不同)http://v2.www.example.com/dir/other.html:不同源(域名不同)http://www.example.com:81/dir/other.html:不同源(端口不同)
总的来说,只要不是三者同时相同,那么就不是同源,那么就会触发同源策略限制。
2.2 同源策略限制了什么
限制了:
- Cookie、LocalStorage 和 IndexDB 无法读取
- DOM 和 JS 对象无法获取
- Ajax 请求发送不出去
这就是我们平常所说的 “跨域问题”。
详细的同源策略相关,可以参考 http://www.ruanyifeng.com/blog/2016/04/same-origin-policy.html
三、解决跨域问题
3.1 使用反向代理方式
3.1.1 什么是反向代理?
反向代理和正向代理的区别:
- 正向代理(Forward Proxy),通常都被简称为代理,就是在用户无法正常访问外部资源,比方说受到 GFW 的影响无法访问 twitter 的时候,我们可以通过代理的方式,让用户绕过防火墙,从而连接到目标网络或者服务。
- 反向代理(Reverse Proxy)是指以代理服务器来接受 Internet 上的连接请求,然后将请求转发给内部网络上的服务器,并将从服务器上得到的结果返回给 Internet 请求连接的客户端,此时,代理服务器对外就表现为一个服务器。
那么可以利用反向代理的原理,我们通过一个中间代理服务器(反向代理服务器),将客户端网络请求的一些 host,domain,port 和协议等东西进行改写,使其模拟为可以访问目标服务器的请求,模拟成不触犯同源策略的请求去请求目标服务器。
3.1.2 如何使用反向代理服务器来解决跨域问题
- 前端 ajax 请求的是本地反向代理服务器
- 本地反向代理服务器接收到后:
- 修改请求的 http-header 信息,例如 referer,host,端口等
- 修改后将请求发送到实际的服务器
- 实际的服务器会以为是同源(参考同源策略)的请求而作出处理
现在前端开发一般使用 nodejs 来做本地反向代理服务器
// 在 express 之后引入路由var app = express();var apiRoutes = express.Router();app.use(bodyParser.urlencoded({extended:false}))// 访问反向代理的路由地址apiRoutes.get("/lyric", function (req, res) { var url = "https://c.y.qq.com/lyric/fcgi-bin/fcg_query_lyric_new.fcg"; // 改写源端的请求信息,然后重定向到目标服务器 axios.get(url, { headers: { // 修改 header referer: "https://c.y.qq.com/", host: "c.y.qq.com" }, params: req.query }).then((response) => { var ret = response.data if (typeof ret === "string") { var reg = /^w+(({[^()]+}))$/; var matches = ret.match(reg); if (matches) { ret = JSON.parse(matches[1]) } } res.json(ret) }).catch((e) => { console.log(e) })});// 使用这个路由app.use("/api", apiRoutes);
这段代码的执行原理是:
- node js 作为反向代理服务器,然后在它上面使用 express 实现路由功能,
- 在 nodejs 里面加入一条负责源端请求的路由映射,将它映射到目标服务器的 api 接口上,并且在这条路由里面将实现请求的改写,模拟目标服务器 api 接口的同源策略所需的要求。
- 源端会先请求 nodejs 反向代理服务器的之前设置的那条路由,会将参数传给他,然后 nodejs 反向代理会将它的请求进行改写,然后转发到目标服务器。
3.2 使用 JSONP 方式
3.2.1 什么是 JSONP
JSONP 有些文章会叫动态创建 script,因为他确实是动态写入 script 标签的内容从而达到跨域的效果:
- AJAX 无法跨域是受到 “同源政策” 的限制,但是带有 src 属性的标签(例如
<script>、<img>、<iframe>
)是不受该政策限制的,因此我们可以通过向页面中动态添加<script>
标签来完成对跨域资源的访问,这也是 JSONP 方案最核心的原理,换句话理解,就是利用了前端请求静态资源的时候不存在跨域问题这个思路。 - JSONP 只能用 get 方式。
- JSONP(JSON Padding) 也叫填充式 JSON,他是 json 的一种使用方式,它允许用户传递一个 callback 参数给服务端,然后服务端返回数据时会将这个 callback 参数作为函数名来包裹住 JSON 数据,这样客户端就可以随意定制自己的函数来自动处理返回数据了。
3.2.1 如何使用 JSONP 来解决跨域问题:
简单一点的例子:
通过不受同源策略限制的标签,例如 script,将一段 js 代码间接地从外部引入。通过 script 标签向目标源发起一个 GET 请求,服务器根据请求的参数返回包含 js 的代码。
//本地代码<script> // 这个函数名字跟服务器返回的那段 js 的函数名字是一样的,所以能够实现调用 function getData(obj) { // 参数是一个对象 var data = JSON.parse(obj); console.log(data.name);//jiavan console.log(data.age);//20 }</script><script src="http://cv.jiavan.com/test/data.php?callback=getData"></script>//服务器上的代码<?php $func = $_GET['callback']; $data = '{"name": "jiavan", "age": 20}'; echo $func."(".$data.");";?>// 服务器返回的数据是一段 js 代码getData( // 这是 js 的函数写法 { // 这是参数,参数是一个对象 "name":"jiavan", "age": 20 })
先在本地定义了一个函数,这是用来处理来自服务器上数据的函数,下面用一个 script 标签,并且向服务器发起了一个 GET 请求,并且指定了处理数据的回调函数,即上方的 getData,服务器收到请求后返回了 getData('{"name": "jiavan", "age": 20}');
,即使一段 js 代码,将数据传入到回调函数中处理,这样便完成了跨域。
参考:https://segmentfault.com/a/1190000004761698
复杂一点的例子:
引用来自 https://segmentfault.com/a/1190000012469713 的图
- 客户端和服务器端约定一个参数名是代表 jsonp 请求的,例如约定 callback 这个参数名。
- 然后服务器端准备好针对之前约定的 callback 参数请求的 javascript 文件,这个文件里面要有一个函数名,要跟客户端请求的时候的函数名要保持一致。(如下面例子:
ip.js
) - 然后客户端注册一个本地运行的函数, 并且函数的名字要跟去请求服务器进行 callback 回调的函数的名字要一致。(如下面例子:foo 函数跟请求时候
callback=foo
的名字是一致的) - 然后客户端对服务器端进行
jsonp 的方式
请求。 - 服务器端返回刚才配置好的 js 文件(
ip.js
)到客户端 - 客户端浏览器,解析 script 标签,并执行返回的 javascript 文件,此时数据作为参数,传入到了客户端预先定义好的 callback 函数里。
- 相当于本地执行注册好 foo 函数,然后获取了一个 foo 函数,并且这个获取的 foo 函数里面包含了传入的参数(例如
foo({XXXXX})
)
- 相当于本地执行注册好 foo 函数,然后获取了一个 foo 函数,并且这个获取的 foo 函数里面包含了传入的参数(例如
服务器端文件 ip.js
foo({ "ip": "8.8.8.8"});
客户端文件 jsonp.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"><html xmlns="http://www.w3.org/1999/xhtml"><head> <title></title> <script> // 动态插入 script 标签到 html 中 function addScriptTag(src) { var script = document.createElement('script'); script.setAttribute("type","text/javascript"); script.src = src; document.body.appendChild(script); } // 获取 jsonp 文件 window.onload = function () { addScriptTag('http://example.com/ip?callback=foo'); } // 执行本地的 js 逻辑,这个要跟获取到的 jsonp 文件的函数要一致 function foo(data) { console.log('Your public IP address is: ' + data.ip); }; </script></head><body></body></html>
3.3 CORS 方式
CORS 是一个 W3C 标准,全称是 "跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器,发出 XMLHttpRequest
请求,从而克服了 AJAX 只能同源使用的限制。
- CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于 IE10。
- 整个 CORS 通信过程,都是浏览器自动完成,不需要用户参与。对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
因此,实现 CORS 通信的关键是服务器端。只要服务器端实现了 CORS 接口,就可以跨源通信。
3.3.1 CORS 的请求分为两类
- 简单请求
- 非简单请求
只要同时满足以下两大条件,就属于简单请求。
(1) 请求方法是以下三种方法之一:
- HEAD
- GET
- POST
(2)HTTP 的头信息不超出以下几种字段:
- Accept
- Accept-Language
- Content-Language
- Last-Event-ID
- Content-Type:只限于三个值 application/x-www-form-urlencoded、multipart/form-data、text/plain
凡是不同时满足上面两个条件,就属于非简单请求。
3.3.2 对简单请求处理
如果是简单请求的话,会自动在头信息之中,添加一个 Origin 字段,Origin 字段用来说明,本次请求来自哪个源(协议 + 域名 + 端口)。服务器根据这个值,决定是否同意这次请求。
GET /cors HTTP/1.1Origin: http://api.bob.com Host: api.alice.comAccept-Language: en-USConnection: keep-aliveUser-Agent: Mozilla/5.0...
如果 Origin 指定的源,不在许可范围内,服务器会返回一个正常的 HTTP 回应。浏览器发现,这个回应的头信息没有包含 Access-Control-Allow-Origin
字段,就知道出错了,从而抛出一个错误,被 XMLHttpRequest
的 onerror
回调函数捕获。注意,这种错误无法通过状态码识别,因为 HTTP 回应的状态码有可能是 200。
这个 Origin 对应服务器端的 Access-Control-Allow-Origin
设置,所以一般来说需要在服务器端加上这个 Access-Control-Allow-Origin
即可,类似这种:
Access-Control-Allow-Origin: http://api.bob.comContent-Type: text/html; charset=utf-8
3.3.3 非简单请求
如果是非简单请求的话,会在正式通信之前,增加一次 HTTP 查询请求,称为 "预检" 请求(preflight)。
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些 HTTP 动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的 XMLHttpRequest 请求,否则就报错。
需要注意这里是会发送 2 次请求,第一次是预检请求,第二次才是真正的请求!
首先发出预检请求:
// 预检请求OPTIONS /cors HTTP/1.1Origin: http://api.bob.comAccess-Control-Request-Method: PUTAccess-Control-Request-Headers: X-Custom-HeaderHost: api.alice.comAccept-Language: en-USConnection: keep-aliveUser-Agent: Mozilla/5.0..
除了 Origin 字段,"预检" 请求的头信息包括两个特殊字段。
(1) Access-Control-Request-Method
该字段是必须的,用来列出浏览器的 CORS 请求会用到哪些 HTTP 方法,上例是 PUT。
(2) Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器 CORS 请求会额外发送的头信息字段,上例是 X-Custom-Header。
然后服务器收到 "预检" 请求以后:
检查了 Origin
、 Access-Control-Request-Method
和 Access-Control-Request-Headers
字段以后,确认允许跨源请求,就可以做出回应。
// 预检请求的回应HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 0139 GMTServer: Apache/2.0.61 (Unix)Access-Control-Allow-Origin: http://api.bob.comAccess-Control-Allow-Methods: GET, POST, PUTAccess-Control-Allow-Headers: X-Custom-HeaderContent-Type: text/html; charset=utf-8Content-Encoding: gzipContent-Length: 0Keep-Alive: timeout=2, max=100Connection: Keep-AliveContent-Type: text/plain
最后一旦服务器通过了 "预检" 请求:
以后每次浏览器正常的 CORS 请求,就都跟简单请求一样,会有一个 Origin 头信息字段。服务器的回应,也都会有一个 Access-Control-Allow-Origin 头信息字段。
// 以后的请求,就像拿到了通行证之后,就不需要再做预检请求了。PUT /cors HTTP/1.1Origin: http://api.bob.comHost: api.alice.comX-Custom-Header: valueAccept-Language: en-USConnection: keep-aliveUser-Agent: Mozilla/5.0...
详情参考这里 http://www.ruanyifeng.com/blog/2016/04/cors.html
总的来说,只需要知道 2 个地方即可,其他的可以触类旁通:
- CORS 需要服务器那边加一个
Access-Control-XXX
的处理,目的是为了处理请求的来源判别。 - CORS 对于非简单请求会增加一次 OPTIONS 的请求。
参考文档:
- 前端解决跨域问题的 8 种方案
- 浏览器同源政策及其规避方法
- https://tonghuashuo.github.io/blog/jsonp.html
- http://www.cnblogs.com/yuzhongwusan/archive/2012/12/11/2812849.html
- http://www.cnblogs.com/dowinning/archive/2012/04/19/json-jsonp-jquery.html
- https://segmentfault.com/a/1190000002438126
- 苹果就“降速门”致歉;央行批扫码支付不正当竞争;王健林旗下公司遭集体裁员
- 姚期智教授:量子计算是千亿万亿级别的产业,或成为科技创新的引擎
- Powershell中禁止执行脚本解决办法
- 使用AsyncTask异步更新UI界面及原理分析
- 商家为何要做小程序?
- Android中关于dip和px以及转换的总结
- Python介绍
- python案例-用户登录
- 推荐个找代码示例的VS 插件 All-In-One Code Framework Sample Browser
- 明星推出定制AI形象,虚拟形象有何优势
- apache工作模式梳理
- Mysql的二进制日志binlog的模式说明
- Git版本控制器使用总结性梳理
- “黑科技”人脸识别 TA和你的距离不是一般的近
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- ThinkPHP like模糊查询,like多匹配查询,between查询,in查询,一般查询书写办法
- PHP设计模式之数据访问对象模式(DAO)原理与用法实例分析
- PHP实现简单登录界面
- PHP创建XML接口示例
- PHP设计模式之单例模式入门与应用详解
- Laravel框架实现定时Task Scheduling例子
- PHP使用 Pear 进行安装和卸载包的方法详解
- Laravel5.1框架注册中间件的三种场景详解
- 浅谈laravel数据库查询返回的数据形式
- PHP设计模式之策略模式(Strategy)入门与应用案例详解
- 基于Python3读写INI配置文件过程解析
- laravel 解决paginate查询多个字段报错的问题
- php判断/计算闰年的方法小结【三种方法】
- Laravel定时任务的每秒执行代码
- PHP学习记录之面向对象(Object-oriented programming,OOP)基础【类、对象、继承等】