浏览器 AJAX 跨域请求访问控制
跨域请求
跨域指的是,__请求的页面与请求服务器__之间发生跨域行为。跨域不仅仅是不同的域名,还包括了协议、端口、子域名。因此在以下情况下都表示了发生跨域行为:
- 协议不同:如
http://
和https://
- 端口不同:如
8080
和8081
- 域名不同:包括一级域名和二级域名,如
a.com
和b.com
,或1.a.com
和2.a.com
- 安全降级:更高的安全级别协议不能向更低的安全级别协议请求资源,即
https://
不能向http://
协议请求资源,反之可以,此种情况无法通过请求头来完成,必须提高对方的安全级别,或自身降级。
同源策略
正因为前端会出现上述的跨域情况,所以有了同源策略(详见:JavaScript 的同源策略)一说。
为什么有同源策略
cookie
因为 JavaScript 有读取浏览器 cookie 的能力,而如果没有同源策略的话,那么你页面上嵌入的第三方脚本将会读取在用户浏览器你的网站所留下的 cookie 信息。举个栗子,你嵌入了一个百度的 JS 文件,而该 JS 文件可以读取你网站的 cookie,后果会怎样?(经小秦读者反馈,该段文字描述有误)
Cookies使用不同的源定义方式。一个页面可以为本域和任何父域设置cookie,只要是父域不是公共后缀(public suffix)即可。Firefox和Chrome使用Public Suffix List决定一个域是否是一个公共后缀(public suffix)。不管使用哪个协议(HTTP/HTTPS)或端口号,浏览器都允许给定的域以及其任何子域名(sub-domains)来访问cookie。设置cookie时,你可以使用Domain,Path,Secure,和Http-Only标记来限定其访问性。读取cookie时,不会知晓它的出处。尽管使用安全的https连接,任何可见的cookie都是使用不安全的连接设置的。 跨域数据存储访问
简而言之,任何嵌入在当前域下的 js 都可以读取到当前域的 cookie。
// 当前域 http://example-2000.com/index.html
<script src="http://example-2000.com/cookie.js"> 可以读取 cookie
<script src="http://example-2001.com/cookie.js"> 可以读取 cookie
公共域问题
- 读:
- 子域可以读取 domain=父域的 cookie
- 父域不能读取 domain=子域的 cookie
- 写:
- 子域可以写 domain=父域的 cookie
- 父域不能写 domain=子域的 cookie
总结:子域可以读写 domain=父域的 cookie,域越短,可操作范围越窄。测试浏览器 chrome。
iframe
如果没有同源策略,百度的网页被嵌入到你的网站,并且你在百度首页上嵌入了你的脚本,你把百度首页改的面目全非,并且还获取了用户搜索信息,后果会怎样?
不作赘述(对于浏览器的同源策略你是怎样理解的呢?)。
哪些情况有同源策略
- cookie
- iframe
- 使用 XMLHttpRequest 发起跨站 HTTP 请求。
- Web 字体 (CSS 中通过 @font-face 使用跨站字体资源)。
- WebGL 贴图
- 使用 drawImage API 在 canvas 上画图
注:以下内容仅重点讨论 AJAX 请求的跨域
访问控制
在有跨域请求的情况下,浏览器为了实现这个要求,为跨域 AJAX 请求加上访问控制。在客户端请求头和服务端响应头上约定一些数据来保证浏览器 AJAX 请求是合法并且被接受的。
// 如果出现跨域了,会进行如下请求
开始请求 ==> 预请求 ===> 主请求
// 如果没有出现跨域,或其他不必进行预请求情况
开始请求 ===> 主请求
预请求
如果出现跨域情况,浏览器会在主请求之前,并且之前请求没有得到认可或认可信息已过期,会根据以下情况决定是否发起一次预请求,请求方法为options。
- 请求以 GET, HEAD 或者 POST 以外的方法发起请求。或者,使用 POST,但请求数据为
application/x-www-form-urlencoded
,multipart/form-data
或者text/plain
以外的数据类型。比如说,用 POST 发送数据类型为application/xml
或者text/xml
的 XML 数据的请求。 - 使用自定义请求头(比如添加诸如 X-Request-With)
以上两种情况,浏览器都会在主请求之前发起一次预请求,预请求的请求方式是OPTIONS
。预请求的请求头包含了以下信息。
Origin
请求域。包含了请求协议、域名、端口。如:
# 如请求页面是 https://1.a.com:8081/foo/bar
Origin: http://1.a.com:8081
注:Origin 也可以为空。正常请求也会携带 Origin 请求头。
Access-Control-Request-Method
主请求的请求方式,如 get、post、delete、put。非预请求的 options。
var xhr = new XMLHttpRequest();
// 请求方式:get
xhr.open('get', 'http://b.com');
xhr.send();
浏览器发起的请求头:
Access-Control-Request-Method: GET
Access-Control-Request-Headers
自定义请求头
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://b.com');
// 自定义请求头
xhr.setRequestHeader('x-request-with', 'XMLHttpRequest');
xhr.send();
浏览器发起的请求头(该值对大小写不敏感):
Access-Control-Request-Headers: x-request-with
withCredentials
标记浏览器是否将凭证(如,cookie)发送到服务器。即 a.com 向 b.com 发起 AJAX 请求,默认是不会将 a.com 的 cookie 一起发送,只有当设置withCredentials = true
时才会发送。
var xhr = new XMLHttpRequest();
xhr.open('get', 'http://b.com');
// 是否发送凭证信息
xhr.withCredentials = true;
xhr.send();
浏览器发起的请求头:
无
预响应
针对以上情况,服务器将在响应头里包含以下信息:
Access-Control-Allow-Origin
信任的跨域来源,与预请求的Origin
匹配。大小写敏感,a.com
与A.COM
是不一样的,但浏览器发起的 Origin 都是小写的。
Access-Control-Allow-Origin: http://1.a.com:8081
如果需要支持多个 Origin,则可以根据请求头的 Origin 做出相应处理即可,如:
if(req.headers.origin === 'http://1.a.com:8081'){
res.setHeader('access-control-allow-origin', req.headers.origin);
}
Access-Control-Allow-Methods
信任的跨域请求方式,与预请求的Access-Control-Request-Methods
匹配。大小写不敏感。
Access-Control-Allow-Methods: GET, POST
Access-Control-Allow-Headers
信任的跨域请求自定义头(即浏览器默认的请求头之外的请求头,通常是x-
开头的),与预请求的Access-Control-Request-Headers
匹配。大小写不敏感,多个值使用,
分隔开。
Access-Control-Allow-Headers: x-request-with
Access-Control-Allow-Headers: x-request-with,content-type
Access-Control-Allow-Credentials
是否接受跨域请求的认证信息,如cookie。可选 true 和 false,其他值都表示 false。大小写敏感。
Access-Control-Allow-Credentials: true
Access-Control-Max-Age
信任跨域的有效期,默认为0,单位秒。即在有效期内,浏览器不会在跨域请求之前发起预请求。
# 60秒
Access-Control-Max-Age: 60
Access-Control-Expose-Headers
信任跨域方能够通过 JavaScript 获取到的自定义头信息。
Access-Control-Expose-Headers: x-test
x-test: 1
JavaScript 获取响应头:
xhr.getResponseHeader('x-test');
// => "1"
主请求
实际请求与正常请求无异。
代码实现
以下代码仅针对正常情况,即请求不包含自定义响应头,不传递 cookie 信息。
前端
基本不需要做任何代码改动。
// jQuery
$.ajax({
url: 'cross-domain-url',
method: 'post',
// 加上 crossDomain 之后,jquery 就不会添加自定义请求头
crossDomain: true
})
// javascript
var xhr = new XMLHttpRequest();
xhr.open('post', 'cross-domain-url');
xhr.send();
后端
处理响应头。
// express
app.use(function (req, res, next) {
res.set('Access-Control-Allow-Origin', '*');
next();
});
// koa
app.use(function*(next){
this.set('Access-Control-Allow-Origin', '*');
yield next;
});
// nginx 反向代理
proxy_set_header Access-Control-Allow-Origin *;
// nginx 普通代理
add_header Access-Control-Allow-Origin *;
// JAVA
response.setHeader("Access-Control-Allow-Origin", "*");
// PHP
header('Access-Control-Allow-Origin *');
总结
AJAX 跨域请求只有高级浏览器下才会支持,低版本的 IE 可以使用 XDomainRequest 构造函数实现,但实现有限,具体细节请谷歌搜索。