【JavaScript编程】浏览器同源策略限制与规避(跨域)

浏览器同源策略


含义

同源策略是浏览器的一个安全功能,不同源的客户端脚本在没有明确授权的情况下,不能读写对方资源。同源 指的是“三个相同”:

  • 协议相同
  • 域名相同
  • 端口相同

举例来说,http://www.example.com/index.html 这个网址,协议是 http://,域名是 www.example.com,端口是 80(默认端口可以省略)。


目的

同源策略的目的,是为了保证用户信息的安全,防止恶意的网站窃取数据。

  设想这样一种情况:A网站是一家银行,用户登录以后,又去浏览其他网站。如果其他网站可以读取A网站的 Cookie,会发生什么?
  很显然,如果 Cookie 包含隐私(比如存款总额),这些信息就会泄漏。更可怕的是,Cookie 往往用来保存用户的登录状态,如果用户没有退出登录,其他网站就可以冒充用户,为所欲为。因为浏览器同时还规定,提交表单不受同源政策的限制。
  由此可见,"同源政策"是必需的,否则 Cookie 可以共享,互联网就毫无安全可言了。


限制范围

随着互联网的发展,"同源政策"越来越严格。目前,如果非同源,共有三种行为受到限制。

  • Cookie、LocalStorage 和 IndexedDB 无法读取。
  • DOM 无法获得。
  • AJAX 请求不能发送。

虽然这些限制是必要的,但是有时很不方便,合理的用途也受到影响。下面,我将详细介绍,如何规避上面三种限制。


一、Cookie


1、浏览器设置

Cookie 是服务器写入浏览器的一小段信息,只有同源的网页才能共享。但是,两个网页一级域名相同,只是二级域名不同,浏览器允许通过设置 document.domain 共享 Cookie。

举例来说,A网页是 http://w1.example.com/a.html ,B网页是 http://w2.example.com/b.html ,那么只要设置相同的 document.domain ,两个网页就可以共享Cookie。

document.domain = 'example.com';

注意:这种方法只适用于 Cookie 和 iframe 窗口,LocalStorage 和 IndexDB 无法通过这种方法,规避同源策略,而要使用下文介绍的 PostMessage API。


2、服务器设置

另外,服务器也可以在设置Cookie的时候,指定Cookie的所属域名为一级域名,如下:

Set-Cookie: key=value; domain=.example.com; path=/

这样的话,二级域名和三级域名不用做任何设置,都可以读取这个Cookie。


二、iframe、LocalStorage


如果两个网页不同源,就无法拿到对方的 DOM。典型的例子是 iframe 窗口和window.open 方法打开的窗口,它们与父窗口无法通信。

  • 如果两个窗口一级域名相同,只是二级域名不同,那么设置上一节介绍的document.domain 属性,就可以规避同源政策,拿到DOM。
  • 对于完全不同源的网站,目前有三种方法,可以解决 跨域窗口的通信问题

1、片段识别符 #

片段标识符指的是,URL 的 # 号后面的部分。我们可以将信息写入到其他窗口的片段标识符,然后该窗口通过读取片段标识符来达到通信的目的。:如果只是改变片段标识符,页面不会重新刷新。


父窗口写入

var src = originURL + '#' + data;
document.getElementById('myIFrame').src = src;

子窗口读取

window.onhashchange = function () {
  	var message = window.location.hash;
  	console.log(message);
}

子窗口改变父窗口的片段标识符

parent.location.href= target + "#" + hash;

2、window.name

无论是否同源,只要在同一个窗口里,前一个网页设置了这个属性,后一个网页就可以读取;

子窗口写入

window.name = data;

父窗口读取

var data = document.getElementById('myFrame').contentWindow.name;

优点:容量很大,可以放置非常长的字符串;

缺点:必须监听子窗口属性的变化,影响网页性能。


3、跨文档通信API

前两种方法属于破解性质,并且使用有很大的局限性。HTML5 为了解决这个问题,引入了一个全新的API:跨文档通信 API。新增了 window.postMessage() 方法,允许跨窗口通信,不论这两个窗口是否同源。


语法

targetWindow.postMessage(message, targetOrigin,[ transfer ])
API描述
targetWindow接收消息的窗口的引用。获得此类引用的方法包括:
Window.open (生成一个新窗口然后引用它);
window.opener (引用产生这个的窗口);
HTMLIFrameElement.contentWindow(从其父窗口引用嵌入式);
window.parent(从嵌入式内部引用父窗口);
window.frames +索引值(命名或数字)。
message要发送到其他窗口的数据。使用结构化克隆算法序列化数据。这意味着您可以将各种各样的数据对象安全地传递到目标窗口,而无需自己序列化。
targetOrigin接收消息的窗口的源,可以是 URI,也可以是 *,表示不限制域名,向所有窗口发送。
transfer可选的。是与消息一起传输的 Transferable 对象序列。这些对象的所有权将提供给目标端,并且它们在发送端不再可用。

实例

例如:父窗口为 http://p.com ,子窗口为 http://c.com

父向子发送信息

// 父文件
var popup = window.open('http://c.com', 'title');
popup.postMessage('How are you?', 'http://c.com');

子向父发送信息

// 子文件
window.opener.postMessage('I am fine, and you?', 'http://p.com');

message 事件

otherWindow 可以通过侦听 message 事件来监听已发送的消息:

window.addEventListener('message', function(event) {
  	console.log(event.data);
}, false);
属性描述
event.data传递的信息。
event.origin消息发向的网址。
event.source对发送消息的窗口的引用;你可以使用它来建立两个不同来源的窗口之间的双向通信。

消息过滤

// 父文件
window.addEventListener('message', receiveMessage);

function receiveMessage(event) {
	if (event.origin !== 'http://p.com') return;
	if (event.data === 'How are you?') {
		event.source.postMessage('Hi', event.origin);
	} 
	else {
		console.log(event.data);
	}
}

延伸(LocalStorage)

// 父窗口发送消息
var win = document.getElementsByTagName('iframe')[0].contentWindow;
var obj = {
	name: 'Guang'
};
// 存入对象
win.postMessage(JSON.stringify({
	key: 'storage',
	method: 'set',
	data: obj
}), 'http://c.com');
// 读取对象
win.postMessage(JSON.stringify({
	key: 'storage',
	method: "get"
}), "*");
window.onmessage = function (e) {
	if (e.origin !== 'http://p.com') return;
	console.log(JSON.parse(e.data).name); // "Guang"
};
// 子窗口接收消息
window.onmessage = function (e) {
	if (e.origin !== 'http://c.com') return;
	var res = JSON.parse(e.data);
	switch (res.method) {
		case 'set':
			localStorage.setItem(res.key, JSON.stringify(res.data));
			break;
		case 'get':
			var parent = window.parent;
			var data = localStorage.getItem(res.key);
			parent.postMessage(data, 'http://p.com');
			break;
		case 'remove':
			localStorage.removeItem(res.key);
			break;
	}
};

三、AJAX 请求


说明:受同源策略的限制,AJAX 请求只能发给同源的网址,否则就报错。

解决:除了架设服务器代理(浏览器请求同源服务器,再由后者请求外部服务),有三种方法规避这个限制。


1、JSONP

基本思想

网页通过添加一个 <script> 元素,向服务器请求 JSON 数据,这种做法不受同源政策限制;服务器收到请求后,将数据放在一个指定名字的回调函数里传回来。

实例

// 动态添加 <script> 元素
function addScriptTag(src) {
	var script = document.createElement('script');
	script.setAttribute("type", "text/javascript");
	script.src = src;
	document.body.appendChild(script);
}

window.onload = function () {
 	// 指定请求地址 以及 回调函数的名称
	addScriptTag('http://example.com/ip?callback=complete');
}

// 回调函数
function complete(data) {
  	console.log(data);
};

说明

  • 动态添加 <script> 元素,向服务器 example.com 发出请求;
  • 该请求的查询字符串有一个 callback 参数,用来指定回调函数的名字,这对于JSONP是必需的;
  • 作为参数的JSON数据被视为JavaScript对象,而不是字符串,因此避免了使用JSON.parse

优点:简单适用,老式浏览器全部支持,服务器改造非常小。

缺点:只能发送 GET 请求


2、WebSocket

WebSocket 是一种通信协议,使用 ws://(非加密)和 wss://(加密)作为协议前缀。该协议不受限于同源策略,只要服务器支持,就可以通过它进行跨源通信。

实例

浏览器发出的 WebSocket 请求的头信息(摘自维基百科):

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13
Origin: http://example.com

说明

Origin:表示该请求的请求源,即发自哪个域名。服务器可以根据这个字段,判断是否许可本次通信。如果该域名在白名单内,服务器就会做出如下回应:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: chat

3、跨域资源共享(CORS)

介绍

CORS 是一个W3C标准,全称是"跨域资源共享"(Cross-origin resource sharing)。它允许浏览器向跨源服务器发出 XMLHttpRequest 请求,从而克服了 AJAX 只能同源使用的限制。

特点

  • CORS 需要浏览器和服务器同时支持。目前,所有浏览器都支持该功能,IE浏览器不能低于IE10。
  • 前端不需要做任何处理,对于开发者来说,CORS通信 与同源的 AJAX通信 没有差别,代码完全一样。
  • 浏览器一旦发现 AJAX 请求跨源,就会自动添加一些附加的头信息,有时还会多出一次附加的请求,但用户不会有感觉。
  • 实现CORS通信的关键是服务器。只要服务器实现了 CORS 接口(做相应配置),就可以跨源通信。

两种请求

浏览器将CORS请求分成两类:简单请求和非简单请求。只要同时满足以下两大条件,就属于简单请求:

  • 请求方法是以下三种方法之一:

    HEAD
    GET
    POST

  • HTTP的头信息中 Content-Type 只限于三个值:

    application/x-www-form-urlencoded
    multipart/form-data
    text/plain

凡是不同时满足上面两个条件,就属于非简单请求。而浏览器对这两种请求的处理,是不一样的。

3.1、CORS 简单请求

浏览器:对于简单请求,浏览器会自动在头信息(Request Headers)之中,增加一个 Origin 字段。用来说明本次请求来自哪个源(协议 + 域名 + 端口):

Origin: http://api.cors.com

服务器:需要指定跨域请求的许可范围 Access-Control-Allow-Origin,值可以是具体的源地址 或者是 *表示所有范围都适用。

var express = require('express');
var app = express();
app.use();

app.all('*', function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    next();
});
  • 如果 Origin 指定的源在许可范围内,服务器返回的响应(Response Headers),会多出几个头信息字段。

    Access-Control-Allow-Origin: http://api.cors.com
    Access-Control-Allow-Credentials: true
    Access-Control-Expose-Headers: FooBar
    
  • 如果 Origin 指定的源,不在许可范围内,服务器会返回的响应头信息不会包含 Access-Control-Allow-Origin 字段,从而抛出一个错误,被 XMLHttpRequest 的 onerror 回调函数捕获。注意,这种错误无法通过状态码识别,因为HTTP回应的状态码有可能是200。
    在这里插入图片描述


3.2、CORS 非简单请求

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


浏览器:对于非简单请求,浏览器会在正式通信之前,先发送一次HTTP查询请求,称为预检请求(
options请求)。如果服务器支持该请求,则会将真正的请求发送到后端,反之,如果浏览器发现服务端并不支持该请求,则会在控制台抛出错误。

服务器配置

var express = require('express');
var app = express();
app.use();

app.all('*', function (req, res, next) {
    res.header("Access-Control-Allow-Origin", "*");
    res.header("Access-Control-Allow-Headers", "Content-Type,X-Requested-With");
    res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
    res.header("Content-Type", "application/json;charset=utf-8");

    next();
});

CORS字段介绍:

字段描述
Access-Control-Allow-Methods该字段必需,它的值是逗号分隔的一个字符串,表明服务器支持的所有跨域请求的方法。注意,返回的是所有支持的方法,而不单是浏览器请求的那个方法。这是为了避免多次预检请求。
Access-Control-Allow-Headers如果浏览器请求包括Access-Control-Request-Headers字段,则Access-Control-Allow-Headers字段是必需的。它也是一个逗号分隔的字符串,表明服务器支持的所有头信息字段,不限于浏览器在"预检"中请求的字段。
Access-Control-Allow-Credentials该字段与简单请求时的含义相同。
Access-Control-Max-Age该字段可选,用来指定本次预检请求的有效期,单位为秒。上面结果中,有效期是20天(1728000秒),即允许缓存该条回应1728000秒(即20天),在此期间,不用发出另一条预检请求。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值