内容简介:笔者经常在前端开源群答疑,加上之前的招聘面试经历。发现许多新手前端在问起跨域问题的解决方案,一套一套的,可是实际遇到跨域问题了就不知道怎么解决了。这次写这篇文章从实践角度聊一聊跨域问题。这句话的三个关键字:
笔者经常在前端开源群答疑,加上之前的招聘面试经历。发现许多新手前端在问起跨域问题的解决方案,一套一套的,可是实际遇到跨域问题了就不知道怎么解决了。这次写这篇文章从实践角度聊一聊跨域问题。
跨域基本概念
出于浏览器的同源策略限制,浏览器会拒绝跨域请求。 这就是跨域问题的产生原因,同源策略是用于隔离潜在恶意文件的重要安全机制。
这句话的三个关键字:
- 同源
- 限制
- 浏览器拒绝
什么是同源
那么第一个问题来了,什么算是同源。解决这个问题需要先了解一下URL的完整结构:
, 该页面访问以下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 面试、工作」看我就 ???? 了 ^_^.
- 数据结构和算法面试题系列—二分查找算法详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Design for Hackers
David Kadavy / Wiley / 2011-10-18 / USD 39.99
Discover the techniques behind beautiful design?by deconstructing designs to understand them The term ?hacker? has been redefined to consist of anyone who has an insatiable curiosity as to how thin......一起来看看 《Design for Hackers》 这本书的介绍吧!