若是你正在开发一个现代的基于web的应用程序,那么你:javascript
XMLHttpRequest cannot load http://external.service/. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://my.app' is therefore not allowed access.java
每次我须要使用一些不能彻底控制的外部服务或一些服务器API集成web 应用程序,我就会碰到这个错误。Google尚未给我提供了关于这个问题一个简洁的描述或替代执行跨域请求的概述,因此这篇文章将做为未来我的参考。git
咱们之因此遇到这个问题,是由于咱们违反了同源策略(SOP)。这是在浏览器实行的一个安全措施,用来限制不一样源之间的文档(或脚本)的交互。github
一个网页的同源是由其定义协议、域名和端口号决定的。例如,本页面的源是(‘http’,‘jvaneyck.wordpress.com’,80).具备相同源的资源能够彻底互相访问。若是页面A和页面B拥有相同的源,那么页面A上的JavaScript能够执行HTTP请求到页面B的服务器,操纵页面B的DOM,甚至读取到页面B的cookies。注意,源是有网页的源地址定义的。阐明:从另外一个域加载的javascript源文件(例如,从远程CDN引用的jQuery)将在HTML页面的源中运行,这个HTML是包含scrip的页面,而不是javascript文件来自的域。web
对于特定的跨域HTTP请求,SOP规定如下通常规则:容许跨域写,禁止跨域读取。这意味着若是A和C是不一样的源,A发送的HTTP请求会由C正确地接收(这些就是“写”),可是A中的脚本将没法读取任何数据--甚至来自C返回的响应代码。这就是跨域“读取”,并被浏览器屏蔽,致使出现上面的错误。换句话说,SOP不阻止攻击者向他们源写数据,它只是不容许他们读取来自你的域的数据(cookie, localStorage 或其余)或利用从他们域接收的响应来作任何事。编程
SOP是一件很是好的事情TM。它能够防止恶意脚本读取你的域的数据,并把其发送到他们的服务器。这意味着一些脚本小子将不能那么轻易地窃取cookies。json
然而,有时你必须有意识地执行跨域请求。提示:这将须要一些额外的工做。合法的跨域请求示例:后端
取决于你在服务器端的控制数量,你有多个选项来启用跨域请求。我将讨论可能的解决方案:JSONP, 使用服务器端代理和CORS。跨域
还有其余选择,使用最普遍的的技术是使用iframes和window.postMessage。我不会再本文讨论,可是对这些感兴趣的能够点击这里。浏览器
我写了一些代码来尝试使用不一样方法,这些均会在本文讨论。你能够在咱们的github中查看完整代码。他们应该很容易在本地运行,若是你想本身尝试的话。每一个例子有2个网站,一个网站在源(‘http’,'localhost',3000)另外一个在(‘http’,'localhost',3001)。他们是不一样的源,因此3000请求3001被认为是跨域请求并被浏览器默认屏蔽。
考虑如下场景:A域的页面想要执行一个GET请求到域B的页面。这就是所发生的:
浏览器把请求正确的发送到服务器:
GET / HTTP/1.1
服务器返回响应:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 Content-Length: 57 { "response": "This is data returned from the server" }
然而在接收到响应,浏览器屏蔽响应进一步传播,并显示同源违反错误如上所示。例如,若是你使用jQuery,对GET请求进行done()回调将永远不会执行,你将没法读取从服务器返回的数据。
JavaScript Object Notation with Padding(简称JSONP)是一种执行跨域请求的方法,经过利用HTML页面的script 标签能够加载来自不一样域的状况。在咱们进入细节以前,我想说它又一些重大的问题:
JSONP依赖这样的事实:<script>标签能够有来自不一样域的资源。当浏览器解析<script>标签,它会GET请求脚本内容(来自任何源)并在当前的页面中执行。一般,服务器会返回HTML或一些显示为数据格式的数据如XML或JSON。然而当向一个启用JSONP的服务器请求时,它会返回一个脚本块,这个脚本块执行一个回调函数,函数已在页面中指定,提供的实际数据做为参数。以防你的脑壳爆炸了,下面的示例会更具体。
源3000的页面想要获取存储在源3001的资源。源3000页面包含下面的script标签:
<script
src='http://localhost:3001?callback=myCallbackFunction'>
</script>
当浏览器解析这个script标签,它将正常的发出GET请求:
GET /?callback=myCallbackFunction HTTP/1.1
服务器无法返回原生JSON,而是返回一个脚本块,包含一个对一个函数的调用,函数名在URL中指定,输出的数据做为参数传递。
HTTP/1.1 200 OK Content-Type: application/javascript myCallbackFunction({'response': 'hello world from JSONP!'});
这个脚本块在浏览器接收到后就当即被执行。在脚本块里的函数调用是在当前页面中评价的。当前页面定义了回调函数,它使用返回的数据:
<script> function myCallbackFunction(data){ $('body').text(data.response); } </script>
总结:
另外一个绕过同源策略执行跨域请求是不作任何跨域请求的!若是你使用了一个代理,这个代理有你的域名,你能够简单地使用它在后端访问外部服务,并把结果返回给你的客户端代码。由于请求代码和代理在同一个域中,因此不违反同源策略。
这种技术不须要改变现有的服务器端代码。它须要有服务器端代理服务,且在相同的域中一样在浏览器中运行JavaScript代码。
为了完整性,我展现一个简短例子:
不是直接向http://localhost:3001执行GET请求,咱们想本身域的代理服务器发送请求。
GET /proxy?urlToFetch=http%3A%2F%2Flocalhost%3A3001 HTTP/1.1
服务器将执行实际的GET请求外部服务。服务器端代码能够正常的执行跨域请求而不会发生错误,所以能够成功的调用。代理服务将结果输送给客户:
HTTP/1.1 200 OK Content-Type: application/json; charset=utf-8 { "response": "This is data returned from the server, proxy style!" }
注意这种方法也有一些严重的缺点。例如,咱们尚未在这篇文章中涉及与安全相关的主题。若是第三方服务使用cookies进行身份严重,那么你就不能使用这种方法。你本身的JavaScript代码是不能访问外部的域的cookies而且cookies也不能发送给你的代理服务,因此不能像第三方服务提供包含用户凭据的cookies。
你可能正感受一个轻微的恶心。若是你以为以前的机制都有“hacky”味道,那么你是绝对正确的。前面的方法都是绕过合法的浏览器安全机制而且绕过它总有写脏。
幸运地是,存在一个更干净的方法: Cross-Origin Resource Sharing(或者简称CORS).
CORS为服务器提供了一个机制来告诉浏览器域A读取请求自域B的数据是能够的。它是经过在响应头中包含一个新的Access-Control-Allow-Origin http 头。若是你还记得引入的错误信息,这正是浏览器试图告诉你的。当浏览器接收到跨域的响应时,它会检查CORS 头。若是响应头中指定的源匹配当前源,它容许读取访问响应。不然,你会获得讨厌的错误信息。
一个具体的例子。
像往常同样请求源3000:
GET / HTTP/1.1
源3001的服务器检查是否这个源能够访问数据,并在响应中增长额外的Access-Control-Allow-Origin头,列出请求源:
HTTP/1.1 200 OK Access-Control-Allow-Origin: http://localhost:3000 Content-Type: application/json; charset=utf-8 Content-Length: 62 { "response": "This is data returned from the CORS server" }
当浏览器接收到响应时它比较请求源(3000)和列在Access-Control-Allow-Origin头的源(也是3000)。因为他们匹配,浏览器容许源3000的代解释执行响应。
像以前同样,这种方法有一些局限性。例如IE比较老的版本只能部分支持CORS. 同时,对全部请求除了最简单请求,你必须有双倍的HTTP请求(参考:preflighting CORS requests)。
在这篇文章中,我试图说明什么类型的请求被分类为跨域请求和在同源策略下他们为何会被浏览器屏蔽。此外,我讨论了几种机制用来执行跨域请求。下表总结了这些机制。
机制 | 支持HTTP方法 | 服务器端修改要求 | 附注 |
JSONP | GET | Yes(返回script块,包含函数调用替代元素JSON) | 须要彻底信任服务器 |
Proxy | ALL | No(可是你的源须要一个额外的代理组件) | 服务器后端执行请求,而不是浏览器。可能会产生进行身份验证的问题 |
CORS | ALL | Yes(返回额外的HTTP 头) | IE较老的版本不支持。更“复杂”的请求,须要额外的HTTP调用(preflighted 请求) |
正如你看到的,即便最简单的跨域请求没有银弹。若是你已经控制服务器代码且你不须要支持遗留的浏览器,我强烈建议你使用CORS方法。一如既往,评你当前的需求,使用最适合你的方法。
译文:Cross-Domain requests in Javascript
不少地方翻译的不对,欢迎指正。