Loading

同源策略和跨域的解决方案

什么是同源?

所谓同源是指:域名协议端口 相同。

检测以下地址和http://www.cnblog.com/ricolee是否同源:

URL 结果 原因
http://www.cnblog.com/ricolee 成功 域名、协议、端口相同
https://www.cnblog.com/ricolee 失败 协议不同
http://www.cnblog.com:8888/ricolee 失败 端口不同
http://www.cnblog.cn/ricolee 失败 域名不同

为什么制定同源策略?

同源策略(Same origin policy)存在于浏览器端是一种约定,由Netscape(网景)提出,用来保护浏览器的数据安全。如果没有同源策略,A网站可以随意访问B网站的Cookie等信息是不安全的,现在所有支持JavaScript 的浏览器都会使用这个策略。

同源策略有什么影响,哪些需要跨域操作?

  • 调用XMLHttpRequest有时候需要跨域,同源策略是禁止使用 XHR 对象向不同源的服务器地址发起 HTTP 请求。
  • fetchAPI通过跨站点方式访问资源,网络字体,例如Bootstrap(通过CSS使用@font-face 跨域调用字体)。
  • 通过canvas标签,绘制图表和视频。
  • DOM操作,同源策略禁止对不同源页面 DOM 进行操作。这里主要场景是 iframe 跨域的情况,不同域名的 iframe 是限制互相访问的。

跨域有风险吗?

跨域请求和Ajax技术都会极大地提高页面的体验,但同时也会带来安全的隐患,其中最主要的隐患来自于CSRF(Cross-site request forgery)跨站请求伪造。

image

CSRF攻击的大致原理是:

  1. 用户通过浏览器,访问正常网站A(例如某银行),通过用户的身份认证(比如用户名/密码)成功A网站;
  2. 网站A产生Cookie信息并返回给用户的浏览器;
  3. 用户保持A网站页面登录状态,在同一浏览器中,打开一个新的TAB页访问恶意网站B;网站B接收到用户请求后,返回一些攻击性代码,请求A网站的资源(例如转账请求);
  4. 浏览器执行恶意代码,在用户不知情的情况下携带Cookie信息,向网站A发出请求。
  5. 网站A根据用户的Cookie信息核实用户身份(此时用户在A网站是已登录状态),A网站会处理该请求,导致来自网站B的恶意请求被执行。

跨域请求出现的错误

例如端口1080的网站请求1090的接口会出现如下错误提示:

`Access to XMLHttpRequest at 'http://localhost:1090/S02CrossDomain/HunterByGet' from origin 'http://localhost:1080' 
has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.`

跨域资源共享(CORS)[推荐]

CORS(Cross-origin resource sharing,跨域资源共享)是一个 W3C 标准,定义了在必须访问跨域资源时,浏览器与服务器应该如何沟通(需要客户端和服务端协同处理)

CORS背后的基本思想,就是使用自定义的 HTTP 头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。CORS需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE 浏览器不能低于IE10

整个CORS通信过程,都是浏览器自动完成,不需要用户参与,对于开发者来说,CORS 通信与同源的 AJAX 通信没有差别,代码完全一样。浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。

因此,实现 CORS 通信的关键是服务器。只要服务器实现了 CORS 接口,就可以跨源通信

CORS浏览器支持情况

image

客户端需要做什么?

基于上述的CSRF的风险,各主流的浏览器都会对动态的跨域请求进行特殊的验证处理。验证处理分为简单请求验证处理和预先请求验证处理。

两种请求

浏览器将CORS请求分成两类:简单请求(simple request)和非简单请求(not-so-simple request)。

只要同时满足以下两大条件,就属于简单请求。

请求方法是下列之一:

  • GET
  • HEAD
  • POST

请求头中的Content-Type请求头的值是下列之一:

  • Accept
  • Accept-Language
  • Content-Language
  • Last-Event-ID
  • Content-Type:只限于三个值application/x-www-form-urlencoded、multipart/form-data、text/plain

凡是不同时满足上面两个条件,就属于非简单请求。

浏览器对这两种请求的处理,是不一样的。

简单请求

基本流程

简单请求时,浏览器会直接发送跨域请求,并在请求头中携带Origin header,表明这是一个跨域的请求。

服务器端接到请求后,会根据自己的跨域规则,通过Access-Control-Allow-OriginAccess-Control-Allow-Methods响应头,来返回验证结果。
如果验证成功,则会直接返回访问的资源内容。

如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应:

image

浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200

image

一般错误控制台会有如下类似提示:
image

如果Origin指定的域名在许可范围内,服务器返回的响应,会多出几个头信息字段。

Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Credentials: true
Access-Control-Expose-Headers: FooBar
Content-Type: text/html; charset=utf-8

withCredentials 属性

默认情况下,跨源请求不提供凭据(cookie、HTTP认证及客户端SSL证明等)。通过将withCredentials属性设置为true,可以指定某个请求应该发送凭据。

一方面,开发者必须在AJAX请求中打开withCredentials属性

xhr.withCredentials = true;

另一方面,如果服务器接收带凭据的请求,会用下面的HTTP头部来响应表示同意。
Access-Control-Allow-Credentials: true
服务器还可以在Preflight响应中发送这个HTTP头部,表示允许源发送带凭据的请求。

image

如果发送的是带凭据的请求,但服务器的响应中没有包含这个头,那么浏览器就不会把响应交给JavaScript(responseText中将是空字符串,size为0)。

image

注意,当withCredentials属性设置为true,需要response header中的'Access-Control-Allow-Origin'为一个确定的域名,而不能使用'*'这样的通配符。

image

非简单请求

预检请求

非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUTDELETE,或者Content-Type字段的类型是application/json

非简单请求的CORS请求,会在正式通信之前,增加一次HTTP查询请求,称为"预检"请求(preflight)。

浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用哪些HTTP动词和头信息字段。只有得到肯定答复,浏览器才会发出正式的XMLHttpRequest请求,否则就报错。浏览器发出的Preflighted requests是一个OPTION请求

OPTIONS请求头部中会包含以下头部:

  • Origin:表示请求来自哪个源。
  • Access-Control-Request-Method必填,用来列出浏览器的CORS请求会用到哪些HTTP方法。
  • Access-Control-Request-Headers:该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段。

预检请求的回应

服务器收到"预检(OPTIONS)"请求以后,检查了OriginAccess-Control-Request-MethodAccess-Control-Request-Headers字段以后,设置比如Access-Control-Allow-MethodAccess-Control-Allow-Headers头部与浏览器沟通来判断是否允许这个请求。

image

如果Preflighted requests验证通过,浏览器才会发送真正的跨域请求。

image

如果Preflighted requests验证失败,则会返回403状态,浏览器不会发送真正的跨域请求。

image

Console查看具体的验证失败原因

image

如果是XMLHttpRequest"预检",浏览器否定了"预检"请求,会返回一个正常的HTTP回应,但是没有任何CORS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。控制台会打印出如下的报错信息

XMLHttpRequest cannot load http://xxx.xxx.com.
Origin http://xxx.xxx.com is not allowed by Access-Control-Allow-Origin.

Request header 有哪些

Origin

头在跨域请求或预先请求中,标明发起跨域请求的源域名。

Access-Control-Request-Method

头用于表明跨域请求使用的实际HTTP方法

Access-Control-Request-Headers

用于在预先请求时,告知服务器要发起的跨域请求中会携带的请求头信息

Response header 有哪些

Access-Control-Allow-Origin

头中携带了服务器端验证后的允许的跨域请求域名,可以是一个具体的域名或是一个*(表示任意域名)。简单请求时,浏览器会根据此响应头的内容决定是否给脚本返回相应内容,预先验证请求时,浏览器会根据此响应头决定是否发送实际的跨域请求。

Access-Control-Expose-Headers

头用于允许返回给跨域请求的响应头列表,在列表中的响应头的内容,才可以被浏览器访问。

Access-Control-Max-Age

用于告知浏览器可以将预先检查请求返回结果缓存的时间,在缓存有效期内,浏览器会使用缓存的预先检查结果判断是否发送跨域请求。

Access-Control-Allow-Credentials

用于告知浏览器当withCredentials属性设置为true时,是否可以显示跨域请求返回的内容。简单请求时,浏览器会根据此响应头决定是否显示响应的内容。预先验证请求时,浏览器会根据此响应头决定在发送实际跨域请求时,是否携带认证信息。

它的值是一个布尔值,表示是否允许发送Cookie.默认情况下,不发生Cookie,即:false。对服务器有特殊要求的请求,比如请求方法是PUT或DELETE,或者Content-Type字段的类型是application/json,这个值只能设为true。如果服务器不要浏览器发送Cookie,删除该字段即可。

Access-Control-Allow-Methods(必须)

用于告知浏览器可以在实际发送跨域请求时,可以支持的请求方法,可以是一个具体的方法列表或是一个*(表示任意方法)。简单请求时,浏览器会根据此响应头的内容决定是否给脚本返回相应内容,预先验证请求时,浏览器会根据此响应头决定是否发送实际的跨域请求。

一个逗号分隔的列表,表明服务器支持的请求类型,比如:GET, POST

Access-Control-Allow-Headers

用于告知浏览器可以在实际发送跨域请求时,可以支持的请求头,可以是一个具体的请求头列表或是一个*(表示任意请求头)。简单请求时,浏览器会根据此响应头的内容决定是否给脚本返回相应内容,预先验证请求时,浏览器会根据此响应头决定是否发送实际的跨域请求。

提供一个逗号分隔的列表表示服务器支持的请求数据类型。假如你使用自定义头部,比如:x-authentication-token 服务器需要在返回OPTIONS请求时,要把这个值放到这个头部里,否则请求会被阻止。

服务端需要做什么?

服务器端对于跨域请求的处理流程如下:

首先查看http头部有无origin字段;
如果没有,或者不允许,直接当成普通请求处理,结束;
如果有并且是允许的,那么再看是否是preflight(method=OPTIONS);
如果不是preflight(简单请求),就返回Allow-Origin、Allow-Credentials等,并返回正常内容。
如果是preflight(预先请求),就返回Allow-Headers、Allow-Methods等,内容为空;

.NET 后端实现CORS 一

在web.config的<system.webServer>节点下加上以下配置(作用于整个网站):

<httpProtocol>
  <customHeaders>
    <add name="Access-Control-Allow-Origin" value="*" />
    <add name="Access-Control-Allow-Headers" value="*" />
    <add name="Access-Control-Allow-Methods" value="GET, POST, PUT, DELETE" />
  </customHeaders>
</httpProtocol>

.NET 后端实现CORS 二

或者在代码中加上如下代码(只作用于当前方法):

public ActionResult HunterAddHeadByCode()
{
    // * 表示允许任何域名跨域访问
    Response.Headers.Add("Access-Control-Allow-Origin", "*");
    Response.Headers.Add("Access-Control-Allow-Headers", "*");
    Response.Headers.Add("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE");
  
    var json = JsonConvert.SerializeObject(Hunters);
    return Json(json, JsonRequestBehavior.AllowGet);
}

.NET 后端实现CORS 三

更好的方式觉得是通过特性实现,哪个接口需要标记特性即可。

待续......

nginx上的CORS配置

location / {
     if ($request method = 'OPTIONS') { 
         add_ header ' Access -Control- Allow-0rigin' *';
         add_ header ” Access Control- Allow-Methods' 'GET, POST, OPTIONS' ;
         add header Access Control Max-Age ' 86400;6 add header ' Content-Type’” text/plain' ;
         add header ' Content-Length’0;
         return 204;
    }
    if ($request_ method = 'GET') {
         add_ header ' Access -Control-Allow-0rigin' 本';
         add header ' Access-Control-Al low-Methods' 'GET, POST, OPTIONS' ;
         add_ header' Access Control -Allow-Headers”'User-Agent , X- Requested -With , Cache - Control , Content -Type;
    }
}

优点

  1. CORS 通信与同源的 AJAX 通信没有差别,代码完全一样,容易维护。
  2. 支持所有类型的 HTTP 请求。

缺点

  1. 存在兼容性问题,特别是 IE10 以下的浏览器。
  2. 第一次发送非简单请求时会多一次请求。

jsonp 跨域

前端实现

Ajax请求加参数dataType: "jsonp"。如需指定特定回调函数就配置jsonpCallback参数。

注意
回调的函数要在window作用域内,否则调用不到。

$.ajax({
        type: "get",
        async: false,
        dataType: "jsonp", //指定服务器返回的数据类型,
        //jsonpCallback: "showData",  //指定回调函数名称
        url: 'http://localhost:1090/S02CrossDomain/HunterByJsonp',
        success: function (res) {
            console.log('success');
            var result = JSON.stringify(res);
            $("#result").html(result);
           
        },
        error: function (data, textStatus, jqXHR) {
            console.log(data);
            $('#result').html(data.statusText);
        }
    });

服务器端实现

服务器端返回值也需要做些修改

public ActionResult HunterByJsonp()
{
    var callback = Request.Params["callback"].ToString();
    Response.ContentType = "application/json;charset=utf-8";
    var json = JsonConvert.SerializeObject(Hunters);
    var result = callback + "(" + json + ")";
    //注:不能用json返回,会报错
    //return Json(result, JsonRequestBehavior.AllowGet);
    return Content(result);
}

提示
ASP.NET MVC 中不能用Json返回否则会报类似错误:jQuery33105546587291303868_1542953995969 was not called

原理解析

页面虽然不允许发起跨域的ajax请求,但引用不同域名的js脚本是可行的。

  1. 执行跨域的ajax请求时会自动发起一个Script请求,请求文件名为callback=jQueryxxx,jQueryxxx是jquery随机生成的一个回调函数名称。
  2. 该次请求返回来的结果则是jQueryxxx()函数调用字符串,执行这个函数完成跨域请求。

一句话来概括就是,通过动态创建script标签,然后利用 src 属性进行跨域,而每一次跨域就是一个script脚本的引入。

下图可以看到下图执行jsonp请求后引用了另一个域的script文件,每次跨域就引用一次:

image

下图为浏览器端收到返回值,执行返回的数据完成跨域操作

image

优点

  • 使用简便,没有兼容性问题

**缺点 **

  • 只支持 GET 请求。
  • 由于是从其它域中加载代码执行,因此如果其他域不安全,很可能会在响应中夹带一些恶意代码。
  • 要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 script 标签新增了一个 onerror 事件处理程序,但是存在兼容性问题。(未验证过)

服务器代理

服务器端是没有跨域限制的,由服务器端请求所需资源再返回客户端。

参考

posted @ 2018-11-23 13:02  牧白  阅读(3253)  评论(0编辑  收藏  举报