首页 > 代码库 > JSONP分享-- 在JavaScript中跨域请求

JSONP分享-- 在JavaScript中跨域请求

    如果你正在开发一个现代的基于web的应用程序,那么你:

  1. 在客户端使用JavaScript。
  2. 需要集成那些没有完全在你控制之下的服务(或者那些来自不同的域)。
  3. 在你的浏览器控制台中遇到过这个错误信息:

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.

    每次我需要使用一些不能完全控制的外部服务或一些服务器API集成web 应用程序,我就会碰到这个错误。Google还没有给我提供了关于这个问题一个简洁的描述或替代执行跨域请求的概述,所以这篇文章将作为将来个人参考。

同源策略

     我们之所以遇到这个问题,是因为我们违反了同源策略(SOP)。这是在浏览器实行的一个安全措施,用来限制不同源之间的文档(或脚本)的交互。

    一个网页的同源是由其定义协议、域名和端口号决定的。例如,本页面的源是(‘http’,‘jvaneyck.wordpress.com’,80).具有相同源的资源可以完全互相访问。如果页面A和页面B拥有相同的源,那么页面A上的JavaScript可以执行HTTP请求到页面B的服务器,操纵页面B的DOM,甚至读取到页面B的cookies。注意,源是有网页的源地址定义的。阐明:从另一个域加载的javascript源文件(例如,从远程CDN引用的jQuery)将在HTML页面的源中运行,这个HTML是包含scrip的页面,而不是javascript文件来自的域。

    对于特定的跨域HTTP请求,SOP规定以下一般规则:允许跨域写,禁止跨域读取。这意味着如果A和C是不同的源,A发送的HTTP请求会由C正确地接收(这些就是“写”),但是A中的脚本将无法读取任何数据--甚至来自C返回的响应代码。这就是跨域“读取”,并被浏览器屏蔽,导致出现上面的错误。换句话说,SOP不阻止攻击者向他们源写数据,它只是不允许他们读取来自你的域的数据(cookie, localStorage 或其他)或利用从他们域接收的响应来做任何事。

     SOP是一件非常好的事情TM。它可以防止恶意脚本读取你的域的数据,并把其发送到他们的服务器。这意味着一些脚本小子将不能那么轻易地窃取cookies。

执行跨域请求

    然而,有时你必须有意识地执行跨域请求。提示:这将需要一些额外的工作。合法的跨域请求示例:

  • 你必须集成第三方服务(如一个论坛),有一个REST API驻留在不同源。
  • 服务器端服务托管在不同的(子)域。
  • 客户端逻辑来自不同源而不是服务器端服务端点。
  • 。。。

    取决于你在服务器端的控制数量,你有多个选项来启用跨域请求。我将讨论可能的解决方案: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 OKContent-Type: application/json; charset=utf-8Content-Length: 57{"response": "This is data returned from the server"}

    然而在接收到响应,浏览器屏蔽响应进一步传播,并显示同源违反错误如上所示。例如,如果你使用jQuery,对GET请求进行done()回调将永远不会执行,你将无法读取从服务器返回的数据。

JSONP

    JavaScript Object Notation with Padding(简称JSONP)是一种执行跨域请求的方法,通过利用HTML页面的script 标签可以加载来自不同域的情况。在我们进入细节之前,我想说它又一些重大的问题:

  • JSONP只能用来执行跨域GET请求。
  • 服务器必须明确地支持JSONP请求。
  • 你必须绝对地信任服务器提供的JSONP响应。
  • 如果服务器被盗用,JSONP可以使你的网站暴露大量的安全漏洞。

    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 OKContent-Type: application/javascriptmyCallbackFunction({‘response‘: ‘hello world from JSONP!‘});

    这个脚本块在浏览器接收到后就立即被执行。在脚本块里的函数调用是在当前页面中评价的。当前页面定义了回调函数,它使用返回的数据:

<script>    function myCallbackFunction(data){        $(‘body‘).text(data.response);    }</script>

总结:

  • 由于JSONP的工作是通过包含一个script标签(无论是纯HTML或编程方式)是由GET请求获取的,它只支持跨域的HTTP GET请求。如果你想使用其他的HTTP请求(像POST, PUT或DELETE),就不能使用JSONP方法。
  • 这个方法要求你必须完全地信任服务器。这个服务器可能被盗用,并返回任意代码,将在你的页面中执行(因此允许访问你的网站cookies, localStorage,等等)。你可以使用frames和window.postMessage执行跨域调用来减轻这种危险。关于具体实现的方法,可以查看这个例子

服务器端代理

    另一个绕过同源策略执行跨域请求是不做任何跨域请求的!如果你使用了一个代理,这个代理有你的域名,你可以简单地使用它在后端访问外部服务,并把结果返回给你的客户端代码。因为请求代码和代理在同一个域中,所以不违反同源策略。

    这种技术不需要改变现有的服务器端代码。它需要有服务器端代理服务,且在相同的域中同样在浏览器中运行JavaScript代码。

    为了完整性,我展示一个简短例子:

    不是直接向http://localhost:3001执行GET请求,我们想自己域的代理服务器发送请求。

GET /proxy?urlToFetch=http%3A%2F%2Flocalhost%3A3001 HTTP/1.1

    服务器将执行实际的GET请求外部服务。服务器端代码可以正常的执行跨域请求而不会发生错误,因此可以成功的调用。代理服务将结果输送给客户:

HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8{"response": "This is data returned from the server, proxy style!"}

    注意这种方法也有一些严重的缺点。例如,我们还没有在这篇文章中涉及与安全相关的主题。如果第三方服务使用cookies进行身份严重,那么你就不能使用这种方法。你自己的JavaScript代码是不能访问外部的域的cookies并且cookies也不能发送给你的代理服务,所以不能像第三方服务提供包含用户凭据的cookies。

CORS

    你可能正感觉一个轻微的恶心。如果你觉得之前的机制都有“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 OKAccess-Control-Allow-Origin: http://localhost:3000Content-Type: application/json; charset=utf-8Content-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方法服务器端修改要求附注
JSONPGETYes(返回script块,包含函数调用替代元素JSON)需要完全信任服务器
ProxyALLNo(但是你的源需要一个额外的代理组件) 服务器后端执行请求,而不是浏览器。可能会产生进行身份验证的问题
CORSALLYes(返回额外的HTTP 头) IE较老的版本不支持。更“复杂”的请求,需要额外的HTTP调用(preflighted 请求)

     正如你看到的,即使最简单的跨域请求没有银弹。如果你已经控制服务器代码且你不需要支持遗留的浏览器,我强烈建议你使用CORS方法。一如既往,评你当前的需求,使用最适合你的方法。

译文:Cross-Domain requests in Javascript

很多地方翻译的不对,欢迎指正。

 

JSONP分享-- 在JavaScript中跨域请求