美文网首页
快速了解JavaScript的跨域

快速了解JavaScript的跨域

作者: 海人为记 | 来源:发表于2021-11-21 11:23 被阅读0次
    跨域.png

    浏览器有一个重要的安全策略,就是 同源策略,它用于限制不同源之间资源的交互。能够帮助阻挡一些恶意的访问,减少可能被攻击的媒介。

    下面看一下与该 URL http://chat.example.com/u1/arts1/101011.html 的源进行对比的示例。

    URL 结果 原因
    http://chat.example.com/u2/arts1/101012.html 同源 路径不同
    http://chat.example.com/u1/arts2/101013.html 同源 路径不同
    https://chat.example.com/u2/arts2/101014.html 失败 协议不同
    http://chat.example.com:88/u1/arts1/101015.html 失败 端口不同(http:// 默认80端口)
    http://news.example.com/u3/arts4/101017.html 失败 主机不同

    因此,默认情况下使用 XMLHttpRequest 进行 Ajax 通信,不能访问除它自己以外的域、协议和端口。但浏览器也需要有合法跨源访问的能力。

    图片探测

    图片探测是利用 <img> 标签实现跨域通信的最早的一种技术。任何页面都可以跨域加载图片而不必担心限制,因此这也是在线广告跟踪的主要方式。可以动态创建图片,然后通过它们的 onloadonerror 事件处理程序得知何时收到响应。

    这种动态创建图片的技术经常用于图片探测(image pings)。图片探测是与服务器之间简单、跨域、单向的通信。数据通过查询字符串发送,响应可以随时设置,不过一般是位图图片或值为 204 的状态码。浏览器通过图片探测拿不到任何数据,但可以通过监听 onloadonerror 事件知道什么时候能接收到响应。下面看一个例子:

    let img = new Image();
    img.onload = img.onerror = function() {
        console.log("已完成!");
    };
    img.src = "http://www.example.com/test?name=Manoa";
    

    这个例子创建了一个新的 Image 实例,然后给 onloadonerror 事件添加同一个函数。这样可确保请求完成时,无论成否都会收到通知。设置完 src 属性之后请求就会开始,这个例子向服务器发送了一个 name 值。

    图片探测频繁用于跟踪用户在页面上的点击操作或动态显示广告。当然,图片探测的缺点是只能发送 GET 请求和无法获取服务器响应的内容。这也是只能利用探测实现浏览器与服务器单向通信的原因。

    JSONP

    JSONP(JSON with padding)是 JSON 的一种变体。用于解决浏览器的跨域问题。需要动态创建 <script> 元素并为 src 属性指定跨域 URL,并将一个回调函数指定在 URL 后面。这样能够不受限制地从其它域加载资源。下面是一个 JSONP 的 URL:

    http://example.com/json/?callback=handleResponse
    

    而想要发送这样的 HTTP 请求,我们就需要实现如下脚本:

    function handleResponse(response) {
        console.log(`
            You're at IP address ${response.ip}, which is in
    ${response.city}, ${response.region_name}`);
    }
    let script = document.createElement("script");
    script.src = "http://example.com/json/?callback=handleResponse";
    document.body.insertBefore(script, document.body.firstChild);
    

    JSONP 由于其简单易用,在开发者中非常流行。相比图片探测,使用 JSONP 可以直接访问响应,实现浏览器与服务器的双向通信。不过 JSONP 也有一些缺点。

    首先,JSONP 是从不同的域拉取可执行代码。如果这个域并不可信,则可能在响应中加入恶意内容。此时除了完全删除 JSONP 没有其它办法。在使用不受控的 Web 服务时,一定要保证是可以信任的。

    第二个缺点是不好确定 JSONP 请求是否失败。虽然 HTML5 规定了 <script> 元素的 onerror 事件处理程序,但还没有被任何浏览器实现。为此,开发者经常使用计数器来决定是否放弃等待响应。这种方式并不准确,毕竟不同用户的网络连接速度和带宽是不一样的。

    CORS 出现之前,实现跨源 Ajax 通信是有点麻烦的。开发者需要依赖能够执行跨源请求的 DOM 特性,在不使用 XMLHttpRequest 对象情况下发送某种类型的请求。虽然 CORS 目前已经得到广泛支持,但这些技术仍然没有过时,因为它们不需要修改服务器。

    跨源资源共享

    在 CORS 出现之前,实现跨源 Ajax 通信有点麻烦。开发者需要依赖能够执行跨源请求的 DOM 特性,在不使用 XMLHttpRequest 的情况下发送某种类型的请求。而 CORS 跨域出现之后,就比较方便了,但也不是说上面介绍的技术已经过时,因为它们不需要修改服务的缘故,还是有使用的场景。下面介绍下 CORS。

    跨源资源共享(CORS,Cross-Origin Resource Sharing)是一种基于 HTTP 头的机制,该机制通过允许服务器自定义 HTTP 头部允许浏览器与服务器实现跨源通信。

    出于安全性,浏览器限制脚本内发起的跨源 HTTP 请求。例如,XMLHttpRequestFetch API 遵循同源策略。这意味着使用这些 API 的 Web 应用程序只能从加载应用程序的同一个域请求 HTTP 资源,而 CORS 是 HTTP 的一部分,它允许服务端来指定哪些主机可以从这个服务端加载资源,因此要响应报文中要包含 CORS 响应头。

    简单请求

    不会触发 CORS 预检请求的请求称为 “简单请求”。“简单请求” 的请求方式是 GETHEADPOST三种之一,首部字段主要是以下字段集合:

    • Accept
    • Accept-Language
    • Content-Language
    • Content-Type:只限 text/plainmultipart/form-dataapplication/x-www-form-urlencoded 三者之一。

    没有自定义头部,请求在发送时会有一个额外的 Origin 头部,其包含发送请求的页面的源,以便服务器确实是否为其提供响应。下面是 Origin 头部的一个示例:

    Origin: http://www.example.com
    

    如果服务器决定响应请求,那么应该发送 Access-Control-Allow-Origin 头部,包含相同的源;或者如果资源是公开的,那么就包含 "*"。比如:

    Access-Control-Allow-Origin: http://www.example.com
    

    如果没有这个头部,或者有但源不匹配,则表明不会响应浏览器请求。否则,服务器就会处理这个请求。注意,无论请求还是响应都不会包含 cookie 信息。

    现代浏览器通过 XMLHttpRequest 对象原生支持 CORS。在尝试访问不同源的资源时,这个行为会被自动触发。要向不同域的源发送请求,可以使用标准 XMLHttpRequest 对象并给 open() 方法传入一个绝对 URL,比如:

    let xhr = new XMLHttpRequest();
    xhr.onreadystatechange = function() {
        if (xhr.readyState == 4) {
            if ((xhr.status >= 200 && xhr.status < 300) || xhr.status == 304) {
                console.log(xhr.responseText);
            } else {
                console.log("请求失败: " + xhr.status);
            }
        }
    };
    xhr.open("get", "http://www.examplecom/page/", true);
    xhr.send(null);
    

    跨域 XMLHttpRequest 对象允许访问 statusstatusText 属性,也允许同步请求。出于安全考虑,跨域 XMLHttpRequest 对象也施加了一些额外限制。

    • 不能使用 setRequestHeader() 设置自定义头部。
    • 不能发送和接收 cookie
    • getAllResponseHeaders() 方法始终返回空字符串。

    因为无论同域还是跨域请求都使用同一个接口,所以最好在访问本地资源时使用相对 URL,在访问远程资源时使用绝对 URL。这样可以更明确地区分使用场景,同时避免出现访问本地资源时出现头部或 cookie 信息访问受限的问题。

    预检请求

    CORS 通过一种预检请求(preflighted request)的服务器验证机制,允许使用自定义头部、除 GET 和 POST 之外的方法,以及不同请求体内容类型。在要发送涉及上述某种高级选项的请求时,会先向服务器发送一个 “预检” 请求。这个请求使用 OPTIONS 方法发送并包含以下头部。

    • Origin:与简单请求相同。
    • Access-Control-Request-Method:请求希望使用的方法。
    • Acces-Control-Request-Headers:(可选)要使用的逗号分隔的自定义头部列表。

    下面是一个假设的 POST 请求,包含自定义的 NCZ 头部:

    Origin: http://www.example.com
    Access-Control-Request-Method: POST
    Access-Control-Request-Headers: NCZ
    

    在这个请求发送后,服务器可以确定是否允许这种类型的请求。服务器会通过在响应中发送如下头部与浏览器沟通这些信息。

    • Access-Control-Allow-Origin:与简单请求相同。
    • Access-Control-Allow-Methods:允许的方法(逗号分隔的列表)。
    • Access-Control-Allow-Headers:服务器允许的头部(逗号分隔的列表)。
    • Access-Control-Max-Age:缓存预检请求的秒数。

    例如:

    Access-Control-Allow-Origin: http://www.example.com
    Access-Control-Allow-Methods: POST, GET
    Access-Control-Allow-Headers: NCZ
    Access-Control-Max-Age: 1728000
    

    预检请求返回后,结果会按响应指定的时间缓存一段时间。换句话说,只有第一次发送这种类型的请求时才会多发送一次额外的 HTTP 请求。“预检请求” 的使用,可以避免跨域请求对服务器的用户数据产生未预期的影响。

    凭据请求

    默认情况下,跨源请求不提供凭据(cookie、HTTP 认证和客户端 SSL 证书)。可以通过将 withCredentials 属性设置为 true 来表明请求会发送凭据。如果服务器允许带凭据的请求,那么可以在响应中包含如下 HTTP 头部:

    Access-Control-Allow-Credentials: true
    

    如果发送了凭据请求而服务器返回的响应中没有这个头部,则浏览器不会把响应交给 JavaScript(responseText 是空字符串,status 是 0,onerror() 被调用)。注意,服务器也可以在预检请求的响应中发送这个 HTTP 头部,以表明这个源允许发送凭据请求。

    HTTP 响应首部字段

    下面列出规范所定义的响应首部字段。

    Access-Control-Expose-Headers

    允许访问该资源的外域 URI。<origin> 参数的值是允许的外域。而设置成通配符*则是服务器允许来自所有域的请求。

    Access-Control-Allow-Origin: <origin> | *
    

    Access-Control-Expose-Headers

    服务器把允许浏览器访问的头放入白名单,浏览器中的XMLHttpRequest就可以通过getResponseHeader访问响应头的 X-My-Custom-HeaderX-Another-Custom-Header

    Access-Control-Expose-Headers: X-My-Custom-Header, X-Another-Custom-Header
    

    Access-Control-Max-Age

    指定了预检请求的结果能够被缓存的秒数。

    Access-Control-Max-Age: <delta-seconds>
    

    Access-Control-Allow-Credentials

    指定了当浏览器的 credentials 设置为 true 时是否允许浏览器读取 response 的内容。当用在对预检请求的响应中时,它指定了实际的请求是否可以使用 credentials

    Access-Control-Allow-Credentials: true
    

    注意:简单 GET 请求不会被预检;如果对此类请求的响应中不包含该字段,响应将被忽略,并且浏览器也不会将相应内容返回给网页。

    Access-Control-Allow-Methods

    首部字段用于预检请求的响应。指明了实际请求所允许使用的 HTTP 方法。

    Access-Control-Allow-Methods: <method>[, <method>]*
    

    Access-Control-Allow-Headers

    首部字段用于预检请求的响应。其指明了实际请求中允许携带的首部字段。

    Access-Control-Allow-Headers: <field-name>[, <field-name>]*
    

    HTTP 请求首部字段

    下面列出规范所定义的可用于发起跨源请求的首部字段。注意:这些首部字段无须手动设置。使用 XMLHttpRequest 时会自动设置。

    Origin

    表明预检请求或实际请求的源(协议、域名和端口)。

    Origin: <origin>
    

    origin 参数的值为源站 URI。它不包含任何路径信息,只是服务器名称。注意:在所有访问控制请求(Access control request)中,Origin 首部字段总是被发送。

    Access-Control-Request-Method

    用于预检请求。其作用是,将实际请求所使用的 HTTP 方法告诉服务器。

    Access-Control-Request-Method: <method>
    

    Access-Control-Request-Headers

    用于预检请求。其作用是,将实际请求所携带的首部字段告诉服务器。

    Access-Control-Request-Headers: <field-name>[, <field-name>]*
    

    总结

    XMLHttpRequest 不允许访问其它域名、协议和端口的资源,访问的话会导致安全错误,这是因为浏览器的同源策略所造成的。而想要跨源访问,就需要使用跨源资源共享方案,XMLHttpRequest原生支持CORS。图片探测和 JSONP 是另两种跨域通信技术,但与 CORS 相比,不是很可靠。

    更多内容请关注公众号「海人为记

    相关文章

      网友评论

          本文标题:快速了解JavaScript的跨域

          本文链接:https://www.haomeiwen.com/subject/ewiptrtx.html