跨域技术与安全

CORS

一、CORS内部机制与实现

CORS(Cross-Origin Resource Sharing,跨域资源共享),它允许浏览器向服务器发出XMLHttpRequest请求,从而克服AJAX只能同源使用的限制。

PS:同源:协议相同,域名相同,端口相同。http://www.example.com/dir/page.html,协议是http://,域名是:/www.example.com,端口是80(默认省略)。非同源,Cookie、LocalStorage 和 IndexDB无法读取,DOM 无法获得,AJAX请求不能发送

http://www.example.com/dir2/other.html:同源
http://example.com/dir/other.html:不同源(域名不同) http://v2.www.example.com/dir/other.html:不同源(域名不同)
http://www.example.com:81/dir/other.html:不同源(端口不同)

CORS需要浏览器和服务器同时支持。目前使用XMLHttpRequest实现的方式(仅支持异步),所有浏览器都支持,但IE10+,低版本的IE有其他实现方式。
CORS的背后思想:使用自定义的HTTP头部,让浏览器与服务器进行沟通,从而决定请求或响应是该成功还是应该失败。在发送请求时,浏览器自行附加一个Origin头部,包含发出请求页面的原信息(协议、域名和端口),然后服务器根据这个头部信息来决定是否给予响应。

客户端发出Origin:http://www.abc.com,如果服务器认为这个请求可以接受,就返回Access-Control-Allow-Origin:http://www.abc.com,如果没有该返回头部,或者头部源信息不匹配,浏览器就会驳回请求。

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

二、两种请求

CORS分为简单请求和非简单请求。
简单请求同时满足以下两大条件:

1.请求方式是以下三种方法之一:
HEAD ,GET, POST
2.HTTP的头部信息不超出以下几种字段:
Accept、Accept-Language、Content-Language、Last-Event-ID、Content-Type:只限于三个值,application/x-www-from-urlencoded(表单序列化)、multipart/form-data(上传文件)、text/plain(纯文本的形式,浏览器在获取到这种文件时并不会对其进行处理)

凡是不同时满足上面两个条件,就是非简单请求。浏览器的处理方式不一样。

三、简单请求

1.基础信息
简单请求,浏览器直接发出CORS请求。在头信息增加Origin字段
浏览器简单AJAX跨域请求:

GET /cors HTTP/1.1
Origin: http://api.bob.com
Host: api.alice.com(接受请求的域)
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

服务器根据Origin的值来决定是否同意请求。如果Origin指定的源,不在许可范围内,服务器会返回一个正常的HTTP响应。浏览器发现,这个回应的头信息没有包含Access-Control-Allow-Origin字段,就知道出错了,从而抛出一个错误,被XMLHttpRequestonerror回调函数捕获。注意,这种错误无法通过状态码识别,因为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

上面的头信息之中,有三个与CORS请求相关的字段,都以Access-Control-开头。
(1)Access-Control-Allow-Origin
该字段是必须的。它的值要么是请求时Origin字段的值,要么是一个*,表示接受任意域名的请求。
(2)Access-Control-Allow-Credentials
该字段可选。它的值是一个布尔值,表示是否允许发送Cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。
(3)Access-Control-Expose-Headers
该字段可选。CORS请求时,XMLHttpRequest对象的getResponseHeader()方法只能拿到6个基本字段:Cache-Control、Content-LanguageContent-TypeExpiresLast-ModifiedPragma。如果想拿到其他字段,就必须在Access-Control-Expose-Headers里面指定。上面的例子指定,getResponseHeader('FooBar')可以返回FooBar字段的值。
2.withCredentials 属性
CORS请求默认不发送Cookie和HTTP认证信息。如果要把Cookie发到服务器,一方面要服务器同意,指定Access-Control-Allow-Credentials字段。

Access-Control-Allow-Credentials:true

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

var xhr = new XMLHttpRequest();   xhr.withCredentials = true;

否则,即使服务器同意发送Cookie,浏览器也不会发送。或者,服务器要求设置Cookie,浏览器也不会处理。
但是,如果省略withCredentials设置,有的浏览器还是会一起发送Cookie。这时,可以显式关闭withCredentials

xhr.withCredentials = false;

需要注意的是,如果要发送Cookie,Access-Control-Allow-Origin就不能设为星号,必须指定明确的、与请求网页一致的域名。同时,Cookie依然遵循同源政策,只有用服务器域名设置的Cookie才会上传,其他域名的Cookie并不会上传,且(跨源)原网页代码中的document.cookie也无法读取服务器域名下的Cookie。

四、非简单请求

1.预检请求
非简单请求时对服务器有特殊要求的请求,例如请求方法是,PUT(让服务器创建一个文件,文件名是请求的url,文件内容是请求报文的主体)|DELETE(删除请求url指定的资源);或者Content-Type字段是application/json
非简单请求,正式通信之前,增加一次HTTP查询请求,叫做“预检请求”
浏览器先询问服务器,当前网页所在的域名是否在服务器的许可名单之中,以及可以使用那些HTTP动词(请求方法)和头部信息字段。只有得到肯定的答复,浏览完才会正式发出XMLHttpRequest请求否则就报错
JS脚本,PUT请求,且发送一个自定义头信息X-Custom-Header

> var url = 'http://api.alice.com/cors';
> var xhr = new XMLHttpRequest();
> xhr.open('PUT', url, true);
> xhr.setRequestHeader('X-Custom-Header', 'value');
> xhr.send();

浏览器发现,这是一个非简单请求,就自动发出一个“预检请求”,要求服务器确认允许这样的请求。下面是一个预检请求的头信息。

OPTIONS /cors HTTP/1.1
Origin: http://api.bob.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: X-Custom-Header
Host: api.alice.com
Accept-Language: en-US
Connection: keep-alive
User-Agent: Mozilla/5.0...

“预检请求”使用的请求方法是OPTIONS,表示这个请求是用来询问的。头信息里面,关键字段是Origin,表示请求来自哪个源。
除了Origin字段,"预检"请求的头信息包括两个特殊字段。
(1)Access-Control-Request-Method
该字段是必须的,用来列出浏览器的CORS请求会用到哪些HTTP方法,上例是PUT
(2)Access-Control-Request-Headers
该字段是一个逗号分隔的字符串,指定浏览器CORS请求会额外发送的头信息字段,上例是X-Custom-Header
2.预检请求回应
服务器收到"预检"请求以后,检查了Origin、Access-Control-Request-Method和Access-Control-Request-Headers字段以后,确认允许跨源请求,就可以做出回应。

HTTP/1.1 200 OK
Date: Mon, 01 Dec 2008 01:15:39 GMT
Server: Apache/2.0.61 (Unix)
Access-Control-Allow-Origin: http://api.bob.com
Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Content-Type: text/html; charset=utf-8
Content-Encoding: gzip
Content-Length: 0
Keep-Alive: timeout=2, max=100
Connection: Keep-Alive
Content-Type: text/plain

上面的HTTP回应中,关键的是Access-Control-Allow-Origin字段,表示http://api.bob.com可以请求数据。该字段也可以设为星号,表示同意任意跨源请求。
如果浏览器否定了预检请求,返回一个正常的HTTP回应,但是么有任何CROS相关的头信息字段。这时,浏览器就会认定,服务器不同意预检请求,因此触发一个错误,被XMLHttpRequest对象的onerror回调函数捕获。
服务器回应的其他CORS相关字段如下。

Access-Control-Allow-Methods: GET, POST, PUT
Access-Control-Allow-Headers: X-Custom-Header
Access-Control-Allow-Credentials: true

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

  1. 浏览器的正常请求和回应
    一旦服务器通过了"预检"请求,以后每次浏览器正常的CORS请求,就都跟简单请求一样,会有一个Origin头信息字段。服务器的回应,也都会有一个Access-Control-Allow-Origin头信息字段。
    下面是"预检"请求之后,浏览器的正常CORS请求。
> PUT /cors HTTP/1.1
> Origin: http://api.bob.com
> Host: api.alice.com
> X-Custom-Header: value
> Accept-Language: en-US
> Connection: keep-alive
> User-Agent: Mozilla/5.0...

上面头信息的Origin字段是浏览器自动添加的。
下面是服务器正常的回应。

> Access-Control-Allow-Origin: http://api.bob.com
> Content-Type: text/html; charset=utf-8

上面头信息中,Access-Control-Allow-Origin字段是每次回应都必定包含的。

五、跨浏览器的CORS实现

所有浏览器都支持简单请求(非预检请求和不带凭据的请求),因此有必要做跨浏览器方案。检测XHR是否支持CORS的最简单方式就是就是检查xhr中是否存在withCredentials属性。再结合XDomainRequest对象(IE10以下)是否存在,就可以兼顾所有浏览器了。

function creaeCORSRequest(method,url){
  var xhr=new XMLHttpRequest();
  if("withCredentials" in xhr){
      xhr.open(method,url,true);
  }else if(typeof XDomainRequest!="undefined"){
     xhr=new XDomainRequest();
      xhr.open(method,url);//没有第三个参数     
  }else{
    xhr=null;
  }
return xhr;
}

var request=createCORSRequest("get","http://www.abc.com");
if(request){
  request.onload=function(){
      console.log(request.responseText);//处理响应信息
  };
request.onerror=function(){
      //处理错误
  }
request.send();
}

onerror和onload分别代替onreadystatechange的检测错误和检测成功

JSONP

1、什么是JSONP
JSONP是JSON with padding的缩写,通过创建script元素来使用,让其src属性为一个跨域的URL,在URL后面添加响应后的回调函数,响应的数据以参数的形式传入回调函数
2、如何使用jsonp跨域

//res响应数据
function handleResponse(res){
    console.log(res)
}
var script=document.createElement('script')
//handleResponse回调函数
script.src="http://freegeoip.net/json/?callback=handleResponse";
document.body.insertBefore(script,document.body.firstChild)

3、JSONP优缺点
优点:能直接访问响应文本,支持在浏览器与服务器之间双向通信。
缺点:其他域可能有恶意脚本,jsonp的请求是否失败不容易无额定
4、cors与jsonp比较
CORS与JSONP的使用目的相同,但是比JSONP更强大。
JSONP只支持GET请求,CORS支持所有类型的HTTP请求。JSONP的优势在于支持老式浏览器,以及可以向不支持CORS的网站请求数据。

图像Ping

1、什么是图像Ping
图像Ping是与服务器进行简单、单向的跨域通信的一种方式,请求的数据通过查询字符串的形式发送,响应可以是任意内容,但通常是像素图或者204(204 No Content成功状态响应码表示目前请求成功,但客户端不需要更新其现有页面。204 响应默认是可以被缓存的。在响应中需要包含头信息 ETag)。通过图像Ping,浏览器得不到任何具体的数据,但通过侦听load和error事件,就能知道响应是什么时候接收到的。
2、实现样例

var img=new Image();
img.onload=img.onerror=function(){
  console.log('done')
}
img.src="http://www.example.com/test?name=Nicholas";

修改document.domain跨子域通信

1、适用情况
当某个页面中包含其他子域的框架或者内嵌框架时,可以通过将每个页面的document.domain设置为相同的值来通信,设置的域名应该是他们的父域。浏览器对domain有一个限制,如果一开始document.domain是松散的,那么就不能再紧绷。例如

//假设页面来自于p2p.wrox.com 域
document.domain = "wrox.com"; //松散的(成功)
document.domain = "p2p.wrox.com"; //紧绷的(出错!)

使用栗子

//html,在http://example.com/a.html中嵌入iframe
<iframe src="http://example.com/b.htm" id=''iframe"  onload = "test()"></iframe>
//http://example.com/a.html中js
 document.domain =‘example.com’
 function test(){
        alert(document.getElementById('iframe').contentWindow);//contentWindow 可取得子窗口的 window 对象
    }
//http://example.com/b.html中js
 document.domain =‘example.com’

跨文档消息传递

1、是什么
跨文档消息传送(cross-document messaging),有时候简称为XDM,指来自不同域的页面间传递消息。一般向包含在当前页面中的iframe元素或者由当前页面弹出的窗口传递。例如:比如www.baidu.com域的A页面通过iframe嵌入了一个google.com域的B页面。XDM核心方法是postMessage(message,tourl)方法,两个参数,第一个是消息,第二个是把消息传给谁。接收到XDM消息后,触发windowonmessage事件。
2、如何用
A页面通过postMessage方法发送消息:

//支持XDM的浏览器也支持iframe的contentWindow属性
window.onload = function() {  
    var ifr = document.getElementById('ifr');  
    var targetOrigin = "http://www.google.com";  
    ifr.contentWindow.postMessage('hello world!', targetOrigin);  
};

B页面通过message事件监听并接受消息:

window.onmessage = function (event) {  
  var data = event.data;//消息  
  var origin = event.origin;//消息来源地址  
  var source = event.source;//源Window对象  
  if(origin=="http://www.baidu.com"){  
console.log(data);//hello world!  
  }  
};

Web Socket

Web Socket目标是在一个单独的持久连接上提供全双工、双向通信(非同源),使用自定义协议,未加密:ws://,加密:wss://。缺点,使用自定义协议,时间较长。

1、实例化

var socket=new WebSocket("ws://www.demo.com/server.php")
实例化后,浏览器会马上尝试创建链接。WebSocket也有readyState属性

0:正在建立连接
1:已经建立连接
2:正在关闭链接
3:已经关闭链接

2、发送和接收数据

发送
只能发送纯文本数据,对象等数据结构需要序列化为JSON字符串。
- 纯文本
socket.send('hello')
- 对象

var obj={
  name:'ok'
}
socket.send(JSON.stringify(message))

接受数据

socket.onmessage=function(event){
  console.log(event.data)//数据在event.data中
}

3、其他事件,在连接的不同生命周期触发

open:成功建立连接
error:发生错误时,连接不能持续
close:连接关闭
必须使用DMO0级定义事件处理程序

4、使用样例

var socket=new WebSocket("wss://www.demo.html/server.js")

socket.onopen=function(){
    console.log("建立连接")
    socket.send('hello')
}
socket.onmessage=function(event){
    console.log(event.data)
}
socket.onerror=function(){
    console.log("错误")
}
socket.onclose=function(){
    console.log("关闭链接")
}

Hash跨域

使用场景,当页面A通过iframe或者frame嵌入了跨域的页面B,我们可以跨域改变B页面的hash。改变hash不会刷新页面,不会请服务器发送请求。

//A中的代码
var B=document.getElementByTagName('iframe')[0]
B.src=B.src+'#'+'data'
//B中的代码
window.onhashchange=function(e){
  var data=window.location.hash
}

安全

一、CSRF

概念

CSRF(Cross-Site Request Forgery)跨站请求伪造

攻击原理
CSRF防御

(1)验证 HTTP Referer 字段

根据 HTTP 协议,在 HTTP 头中有一个字段叫 Referer,它记录了该 HTTP 请求的来源地址

(2)在请求地址中添加 token 并验证

CSRF 攻击之所以能够成功,是因为黑客可以完全伪造用户的请求,该请求中所有的用户验证信息都是存在于 cookie 中,因此黑客可以在不知道这些验证信息的情况下直接利用用户自己的 cookie 来通过安全验证。要抵御 CSRF,关键在于在请求中放入黑客所不能伪造的信息,并且该信息不存在于 cookie 之中。可以在 HTTP 请求中以参数的形式加入一个 随机产生token,并在服务器端建立一个拦截器来验证这个 token,如果请求中没有 token 或者 token 内容不正确,则认为可能是 CSRF 攻击而拒绝该请求。

(3)每次请求都要附带经过相应算法计算得到的验证码

二、XSS

概念

跨站脚本攻击(Cross Site Scripting)。

原理

往页面中 注入恶意script代码,当代码被浏览器解析执行时便达到攻击的目的。

防御

不能原样的将用户输入的数据直接存到服务器,需要对数据进行一些处理。

  • 过滤危险的DOM节点。如具有执行脚本能力的script, 具有显示广告和色情图片的img, 具有改变样式的link, style, 具有内嵌页面的iframe, frame等元素节点。
  • 过滤危险的属性节点。如事件, style, src, href等
  • 对cookie设置httpOnly。

参考资料

JavaScript高级程序设计第三版

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 161,873评论 4 370
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 68,483评论 1 306
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 111,525评论 0 254
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,595评论 0 218
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 53,018评论 3 295
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,958评论 1 224
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 32,118评论 2 317
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,873评论 0 208
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,643评论 1 250
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,813评论 2 253
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,293评论 1 265
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,615评论 3 262
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,306评论 3 242
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,170评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,968评论 0 201
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 36,107评论 2 285
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,894评论 2 278

推荐阅读更多精彩内容