同源策略和跨域的解决方案
什么是同源?
所谓同源是指:域名、协议、端口 相同。
检测以下地址和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)跨站请求伪造。
CSRF攻击的大致原理是:
- 用户通过浏览器,访问正常网站A(例如某银行),通过用户的身份认证(比如用户名/密码)成功A网站;
- 网站A产生Cookie信息并返回给用户的浏览器;
- 用户保持A网站页面登录状态,在同一浏览器中,打开一个新的TAB页访问恶意网站B;网站B接收到用户请求后,返回一些攻击性代码,请求A网站的资源(例如转账请求);
- 浏览器执行恶意代码,在用户不知情的情况下携带Cookie信息,向网站A发出请求。
- 网站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浏览器支持情况
客户端需要做什么?
基于上述的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-Origin
和Access-Control-Allow-Methods
响应头,来返回验证结果。
如果验证成功,则会直接返回访问的资源内容。
如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP回应:
浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段(详见下文),就知道出错了,从而抛出一个错误,被XMLHttpRequest的onerror回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200
一般错误控制台会有如下类似提示:
如果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头部,表示允许源发送带凭据的请求。
如果发送的是带凭据的请求,但服务器的响应中没有包含这个头,那么浏览器就不会把响应交给JavaScript(responseText中将是空字符串,size为0)。
注意,当withCredentials属性设置为true,需要response header中的'Access-Control-Allow-Origin'为一个确定的域名,而不能使用'*'这样的通配符。
非简单请求
预检请求
非简单请求是那种对服务器有特殊要求的请求,比如请求方法是PUT
或DELETE
,或者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)"请求以后,检查了Origin
、Access-Control-Request-Method
和Access-Control-Request-Headers
字段以后,设置比如Access-Control-Allow-Method
、Access-Control-Allow-Headers
头部与浏览器沟通来判断是否允许这个请求。
如果Preflighted requests验证通过,浏览器才会发送真正的跨域请求。
如果Preflighted requests验证失败,则会返回403状态,浏览器不会发送真正的跨域请求。
Console查看具体的验证失败原因
如果是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;
}
}
优点
- CORS 通信与同源的 AJAX 通信没有差别,代码完全一样,容易维护。
- 支持所有类型的 HTTP 请求。
缺点
- 存在兼容性问题,特别是 IE10 以下的浏览器。
- 第一次发送非简单请求时会多一次请求。
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
脚本是可行的。
- 执行跨域的ajax请求时会自动发起一个Script请求,请求文件名为callback=jQueryxxx,jQueryxxx是jquery随机生成的一个回调函数名称。
- 该次请求返回来的结果则是jQueryxxx()函数调用字符串,执行这个函数完成跨域请求。
一句话来概括就是,通过动态创建script标签,然后利用 src 属性进行跨域,而每一次跨域就是一个script脚本的引入。
下图可以看到下图执行jsonp
请求后引用了另一个域的script文件,每次跨域就引用一次:
下图为浏览器端收到返回值,执行返回的数据完成跨域操作
优点
- 使用简便,没有兼容性问题
**缺点 **
- 只支持 GET 请求。
- 由于是从其它域中加载代码执行,因此如果其他域不安全,很可能会在响应中夹带一些恶意代码。
- 要确定 JSONP 请求是否失败并不容易。虽然 HTML5 给 script 标签新增了一个 onerror 事件处理程序,但是存在兼容性问题。(未验证过)
服务器代理
服务器端是没有跨域限制的,由服务器端请求所需资源再返回客户端。