阅读 1506

跨域技术(上下完整版)

跨域技术(上)

跨域技术(上)包括 同源策略及其限制什么是跨域图像PingJSONPCORSWebSocket 等跨域技术。

(一)同源策略及其限制

同源策略

点击查看浏览器同源策略官方又臭又长说法

同源策略限制内容

  • Cookie、LocalStorage、IndexedDB 等存储性内容
  • DOM 节点获取
  • AJAX 请求的发送

(二)什么是跨域

所谓同源是指:协议,域名,端口都相同,就是同源;

协议,域名,端口号有一个不同就是跨域

跨域并不是请求发不出去,请求能发出去,服务端能收到请求并正常返回结果,只是结果被浏览器拦截了。

可能嵌入跨源的资源的标签

<script src="..."></script> | 标签嵌入跨域脚本。

<link rel="stylesheet" href="..."> | 标签嵌入CSS。

<img> | 嵌入图片。支持的图片格式包括PNG,JPEG,GIF,BMP,SVG,...

<video><audio> | 嵌入多媒体资源。

<object>, <embed><applet> 的插件。

<frame><iframe> | 载入的任何资源。站点可以使用X-Frame-Options消息头来阻止这种形式的跨域交互。

@font-face 引入的字体 | 一些浏览器允许跨域字体(cross-origin fonts),一些需要同源字体(same-origin fonts)。

background: url(); 背景链接 | 嵌入的资源可能存在跨域

可能存在跨源的场景

  • 资源跳转: A链接跳转、重定向、表单提交
  • 资源嵌入: <link><script><img><frame>等dom标签
  • 脚本请求: ajax请求、dom和js对象的跨域操作等

(三)几种跨域技术的讨论

图像Ping

1.原理

  • 我们知道,一个网页可以从任何网页中加载图像,不用担心跨域不跨域。这也是在线广告跟踪浏览量的主要方式。
  • 可以动态地创建图像,使用它们的onloadonerror 事件处理程序来确定是否接收到了响应。
  • 动态创建图像经常用于图像 Ping。图像 Ping 是与服务器进行简单、单向的跨域通信的一种方式。

2.具体步骤

请求的数据是通过查询字符串形式发送的,而响应可以是任意内容,但通常是像素图或 204 响应。通过图像 Ping,浏览器得不到任何具体的数据,但通过侦听 load 和 error 事件,它能知道响应是什么时候接收到的。

let img = new Image(); //创建了一个 Image 的实例
// 将 onload 和 onerror 事件处理程序指定为同一个函数
img.onload = img.onerror = function(){ 
    alert("Done!"); 
}; 
// 请求中发送了一个 name 参数
img.src = "http://www.example.com/test?name=Nicholas";
复制代码

3.应用场景

图像 Ping 最常用于跟踪用户点击页面或动态广告曝光次数。

4.图像 Ping 的缺点

  • 一是只能发送 GET 请求,
  • 二是无法访问服务器的响应文本。因此,图像 Ping 只能用于浏览器与服务器间的单向通信。

JSONP(JSON with padding 填充式JSON)

1.JSONP的作用、组成

  • JSONP能够让网页从别的地址(跨域的地址)那获取资料,即跨域读取数据
  • JSONP 由两部分组成:回调函数和数据。
    • 回调函数是当响应到来时应该在页面中调用的函数。回调函数的名字一般是在请求中指定的。
    • 而数据就是传入回调函数中的JSON数据。

2.JSONP实现跨域访问的原理及优化

2.1 <script>标签的作用

  • 在同一界面中可以定义多个<script>标签,同一个界面中多个<script>标签中的数据可以相互访问
  • 可以通过<script>的src属性导入其它资源,通过src属性导入其它资源的本质就是将资源拷贝到<script>标签中
  • <script>的src属性不仅能导入本地资源, 还能导入远程资源
  • 由于<script>的src属性没有同源限制, 所以可以通过<script>的src属性来请求跨域数据

2.2 JSONP原理

JSONP 是通过动态<script>元素来使用的,使用时可以为 src 属性指定一个跨域 URL。这里的<script>元素与<img>元素类似,都有能力不受限制地从其他域加载资源。因为 JSONP 是有效的 JavaScript代码,所以在请求完成后,即在 JSONP 响应加载到页面中以后,就会立即执行。

2.3 JSONP优化

2.3.1 为什么要优化(存在的问题)?

  • 通过JSONP来获取跨域的数据,一般情况下服务器返回的都不会是一个变量,而是一个函数的调用
  • 当前服务器返回的函数调用名称如果是固定的,服务器返回函数叫什么名称,我们本地就必须定义一个叫什么名称的函数
  • 由于<script>标签默认是同步,前面的<script>标签没有加载完数据,后面的<script>标签就不会被执行, 所以请求数据的<script>标签必须放到后面

2.3.2 解决方案:

  • 通过URL参数的方式来动态指定函数名称
  • 通过JS动态创建<script>标签,因为JS动态创建的<script>标签默认就是异步的, 不用等到前面的标签加载完就可以执行后面的<script>标签

2.3.3 实现方式

原生实现

<script>
    //jsonp回调方法,一定要写在jsonp请求前面
    function testjson(txt){
    	alert(txt);
    }
</script>

<script>
    //动态创建<script>标签
    let oScript = document.createElement("script");
    oScript.src = "http://127.0.0.1:80/jQuery/Ajax/20-jsonp.php?cb=test";
    document.body.appendChild(oScript);
</script>
复制代码

jQuery方式实现

jQuery中的Ajax可以设置为请求跨域的数据

  • dataType: "jsonp" | 设置请求方式为jsonp,告诉jQuery需要请求跨域的数据。
  • jsonp: "cb" | 告诉jQuery服务器在获取回调函数名称的时候需要用什么key来获取
  • jsonpCallback: "handleCallback" | 自定义回调函数名,告诉jQuery服务器在获取回调函数名称的时候回调函数的名称是什么
<script>
    $.ajax({
        url: "http://127.0.0.1:80/jQuery/Ajax/22-jsonp.php",
        data:{
            "name": "ghk",
            "age": 18
        },
        dataType: "jsonp",   // 设置请求方式为jsonp
        jsonp: "cb",  
        jsonpCallback: "handleCallback", // 自定义回调函数名
        success: function (msg) {
            console.log(msg);
        }
    });
</script>
复制代码

3.JSONP的优缺点

3.1 优点

与图像 Ping 相比,它的优点在于能够直接访问响应文本,支持在浏览器与服务器之间双向通信。

3.2 缺点

  • JSONP 是从其他域中加载代码执行。如果其他域不安全,很可能会在响应中夹带一些恶意代码,而此时除了完全放弃 JSONP 调用之外,没有办法追究。
  • 不能注册success、error等事件的监听,所以要确定 JSONP 请求是否失败并不容易
  • JSONP 只支持get请求(JSONP的实现原理就是创建一个script标签,再把需要请求的api地址放到src里. 所以只能是get请求.)

CORS (Cross-origin resource sharing 跨域资源共享)

1.CORS是什么及原理

谁应该读这篇文章? 说实话,每个人。

一句话总结:CORS是跨域的根本解决方法(处理跨域问题的标准做法),由浏览器自动完成。

CORS是一个W3C标准,它允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。

CORS背后的基本思想:使用自定义的HTTP头部让浏览器与服务器进行沟通,从而决定请求或响应是应该成功,还是应该失败。

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

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

2.CORS的两种请求

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

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

(1) 请求方法是以下三种方法之一:

  • HEAD
  • GET
  • POST

(2) HTTP的头信息不超出以下几种字段:

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

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

浏览器对这两种请求的处理,是不一样的。(阮老师这里有对这两种请求的详细解说)

3.实现

普通跨域请求:服务端设置Access-Control-Allow-Origin即可,前端无须设置;若要带cookie请求,前后端都需要设置。

在响应头上添加 Access-Control-Allow-Origin 属性,指定同源策略的地址。同源策略默认地址是网页的本身。只要浏览器检测到响应头带上了CORS,并且允许的源包括了本网站,那么就不会拦截请求响应。

原生实现

let xhr = new XMLHttpRequest();
xhr.withCredentials = true;// 前端设置是否带cookie
...
复制代码

jQuery Ajax

$.ajax({
    ...
    xhrFields: {
        withCredentials: true,  // 前端设置是否带cookie
    },
    crossDomain: true, // 会让请求头中包含跨域的额外信息,但不会含cookie
    ...
});
复制代码

4.CORS优缺点

CORS要求浏览器(>IE10)和服务器的同时支持,是跨域的根本解决方法,由浏览器自动完成。 优点在于功能更加强大支持各种HTTP Method,缺点是兼容性不如JSONP。

WebSocket

同源策略对 WebSocket 不适用,因此可以通过它打开到任何站点的连接。

1.WebSocket的由来

为了解决服务器向浏览器端及时推送消息的需求,提出了一些替代的、变通的解决方案,如iframe、AJAX,基本思路是浏览器端及时提交请求,服务器收到请求后进行阻塞,并保持连接,等待有消息需要发送时,及时向浏览器端进行响应推送。 然而,这些技术都是基于HTTP协议实现。从根本上讲,HTTP是半双工的协议,也就是说,在同一时刻信息只能单向传输。浏览器端向服务器发送请求(单向),然后服务器响应请求(单向)。这样,服务器只能被动的接收到请求信息后,再向浏览器端响应请求,而无法主动向客户端推送信息。

然而,随着人们对信息的实时性要求越来越高(如在线订票系统、即时通信系统和股票交易系统等),当服务器数据发生变化的时候,需要主动、实时地向浏览器端发送消息,将最新的数据或事件通知给用户。显然,HTTP作为半双工的通信协议并不能高效地支持上述需求功能的实现。我们需要一种高效节能的全双工通信机制来保证数据的实时传输。因此,WebSocket应运而生。

2.WebSocket的优点和应用场景

2.1 WebSocket的优点

WebSocket作为一种高效的全双工通信机制,有如下优点

  • WebSocket通过一个单一的套接字在Web上进行操作,以最小的开销高效地提供了Web连接。相较于经常需要使用推送实时数据到浏览器端的轮询、长轮询及长连接来说,这就极大地减少了不必要的网络流量
  • 实现了浏览器与服务器的双向通道服务器和浏览器端可以主动的发送数据给对方
  • WebSocket减少了延迟,因为一旦建立起WebSocket连接,服务器可以在消息可用时发送它们,不用频繁创建TCP请求及销毁请求,减少网络带宽资源的占用,同时也节省服务器资源。
  • WebSocket减少了网络带宽资源的占用,因为无需频繁创建及销毁
  • WebSocket节省了服务器资源。因为一旦建立起WebSocket连接,服务器可以在后台数据更新时及时推送消息给客户端。

2.2 应用场景

通过WebSocket实现的浏览器后台传输技术,可以应用于如多媒体聊天、实时状态监控、股票行情、基于位置的应用等需要实时刷新的应用场景。

3.WebSocket协议通信机制

WebSocket协议是独立的、基于TCP的协议。其本质是先通过HTTP/HTTPS协议进行握手后创建一个用于交换数据的TCP连接,此后服务器端与浏览器端通过此TCP连接进行实时通信。

图所示为WebSocket的通信模式:

浏览器端首先向服务器端发起一个HTTP请求。这个请求包含了一些附加头信息,是一个升级的HTTP请求。服务器端解析这些附加头信息后,产生应答信息返回给浏览器端。这样,浏览器端和服务器端之间的WebSocket连接就建立起来了。双方可以通过这个连接通道自由地传输数据,直到浏览器端或服务器端主动关闭该连接。

4.WebSocket协议通信实现的相关技术

4.1 WebSocket构造函数

4.1.1 客户端要和服务器端建立WebSocket连接,只需使用构造函数建立一个WebSocket对象,并且为这个对象提供连接端口的URL地址即可。

let  ws = new  WebSocket("ws://127.0.0.1:8080/WebSocket/WebSocket")
复制代码

4.1.2 注意点

  • URL地址的字符串需要以“ws”或“wss”(加密通信时使用)作为开头。如果URL地址有语法错误,那么构造函数将会抛出异常。

  • 必须给 WebSocket 构造函数传入绝对 URL。同源策略对 Web Sockets 不适用,因此可以通过它打开到任何站点的连接。至于是否会与某个域中的页面通信,则完全取决于服务器。

4.2 WebSocket的readyState属性

readyState 属性返回实例对象的当前状态,共有四种。

状态 含义
WebSocket.OPENING (0) 正在建立连接。
WebSocket.OPEN (1) 已经建立连接。
WebSocket.CLOSING (2) 正在关闭连接。
WebSocket.CLOSE (3) 已经关闭连接。

4.3 WebSocket事件

  • open 事件(WebSocket连接建立后触发该事件)
    一旦服务器响应了客户端的WebSocket连接请求,open事件就会被触发。而要监听open事件,只需要为其添加回调函数。

    ws.onopen = function(){
        setConnected(true));
    };
    复制代码
  • message 事件(当客户端接收到服务器的数据时触发该事件)
    当客户端接收到服务器发送的消息后,message事件触发,并且客户端可调用相应函数对所接收的消息进行处理。

    ws.onmessage= function(e){
        console.log("接收:"+e.data);
    }
    复制代码
  • error 事件(当通信过程中发生错误时触发该事件)
    error事件会在WebSocket连接出现故障时触发,用来处理异常事件。

    ws.onerror = function(e){
        console.log("WebSocket Error: ", e);
    };
    复制代码
  • close 事件(当连接关闭时触发该事件)。
    如果接收到error事件,可以预期很快会触发close事件。close事件在WebSocket连接关闭时触发,处理程序如下所示:

    ws.onclose = function(e){
        setConnected(false));
    };
    复制代码

    一旦连接关闭,客户端和服务器不再接收或者发送消息。

4.4 WebSocket方法

WebSocket对象有两个方法即send()和close(),分别用来发送数据和关闭连接。

  • send() 方法
    WebSocket在客户端和服务器之间建立连接后,就可以调用send()方法来传输数据,语法格式如下所示:
    ws.send("message");
    复制代码
  • close() 方法
    使用close()方法,可以关闭WebSocket连接,语法格式如下所示:
    ws.close(code, reason);
    复制代码
    可以在close()方法传递两个可选参数:code(数字型的状态代码)和reason(一个文本字符串),用来说明关闭连接的原因。

5.WebSocket小结

有没有发现好用的东西、牛逼的东西总得要有点小缺陷才行,例如兼容问题,不然开发就没啥意思了(不是

JavaScript 中创建了 WebSocket 之后,会有一个 HTTP 请求发送到浏览器以发起连接。在取得服务器响应后,建立的连接会使用 HTTP 升级从 HTTP 协议交换为 Web Socket 协议。

也就是说,使用标准的 HTTP 服务器无法实现 WebSocket ,只有支持这种协议的专门服务器才能正常工作

目前支持WebSocket的服务器有很多,包括了Jetty7、Netty、mod pyWebSocket、Nodejs和Tomcat7等。在双向通道建立后,服务器端同样采用事件驱动的架构模式,监听事件、并触发相应方法函数来向客户端推送数据。


本文参考书籍:

《JavaScript高级程序设计》

本文参考链接:

浏览器的同源策略 MDN

HTTP访问控制(CORS) MDN

跨域资源共享 CORS 详解 阮一峰

WebSocket 教程 阮一峰

不要再问我跨域的问题了



跨域技术(下)

跨域技术(下)包括 iframe标签的使用document.domainwindow.namelocation.hashpostMessage等跨域技术。

iframe标签

域名地址的组成

iframe 标签

iframe是一种HTML标记,它会创建包含另外一个文档的内联框架,通过iframe框架可以在当前页面中显示其他页面的信息。

将iframe的src属性设置为对另外一个页面的连接请求,并在当前页面中通过JavaScript动态更新iframe的内容,就可以将服务器端的数据响应到客户端且而不会出现主页面一片空白,等待刷新的现象。

并且,仅刷新iframe框架而不是主页面,也减少了页面刷新的内容,这在一定程度上提高了页面刷新速度。

iframe 标签的使用

1.用iframe嵌套页面时,如果父页面要获取子页面里面的内容,可以使用 contentWindow 或者contentDocument

子页面获取父页面使用window.parent

//父窗口
<iframe id="father" src="iframe.html"></iframe>
<script>
    console.log(iframe.contentWindow); //获取子页面里面的内容
    console.log(iframe.contentDocument);
</script>

//子窗口
<div id="son">内容</div>
<script>
  console.log(window.parent); //获取父页面内容
</script>
复制代码

2.页面间通信,获取子页面的内容、cookie等

//父窗口
<iframe id="father" src="iframe.html"></iframe>
<script>
  let iframe = document.getElementById("father");
  iframe.onload = function(){
    let doc = iframe.contentWindow.document;
    console.log(doc.getElementById('son').innerHTML);//'内容'
    console.log(document.cookie);//'name=match'
  }
</script>

//子窗口
<div id="son">内容</div>
<script>
  document.cookie = 'name=match';
</script>
复制代码

document.domain + iframe(主域相同的跨域)

这种方式只适合主域名相同,子域名不同的iframe跨域

通过修改document的domain属性,我们可以在域和子域或者不同的子域之间通信。同域策略认为域和子域隶属于不同的域,比如www.a.com和sub.a.com是不同的域,这时,我们无法在www.a.com下的页面中调用sub.a.com中定义的JavaScript方法。但是当我们把它们document的domain属性都修改为a.com,浏览器就会认为它们处于同一个域下,那么我们就可以互相调用对方的method来通信了。

document.domain实现跨域核心

1.主域相同而二级域名不同的情况下,两个文件分别加上 document.domain = "example.com";然后通过 a.html 文件创建一个 iframe,去控制 iframe 的 window,从而进行交互,这种方法只能解决主域相同而二级域名不同的情况。

2.注意

使用document.domain允许子域安全访问其父域时,您需要设置document域在父域和子域中具有相同的值。这是必要的,即使这样做只是将父域设置回其原始值。否则可能会导致权限错误。这里都是a.com。

// 在www.a.com/a.html中:
<script>
    document.domain = 'a.com';
    let ifr = document.createElement('iframe');
    ifr.src =  'http://www.script.a.com/b.html';  
    ifr.display = none;
    document.body.appendChild(ifr);
    ifr.onload = function(){ 
        let doc = ifr.contentDocument || ifr.contentWindow.document;  
        ifr.onload = null;
};
</script>
复制代码
// 在www.script.a.com/b.html中:
<script>
    document.domain ='a.com';
    window.data = '传送的数据:1111';
</script>
复制代码

window.name + iframe

window.name 属性用于获取/设置窗口的名称。该属性有个特征:即在一个窗口(window)的生命周期内,窗口载入的所有的页面都是共享一个window.name的,每个页面对window.name都有读写的权限,window.name是持久存在一个窗口载入过的所有页面中的。window.name属性的神奇之处在于name值在不同的页面(甚至不同域名)加载后依旧存在(如果没修改则值不会变化),并且可以支持非常长的 name 值(2MB)。

当该window的location变化,然后重新加载,它的name属性可以依然保持不变。

在控制台输入:

页面跳转到百度时,name属性值不变

window.name实现跨域核心

可以将跨域请求用如下步骤解决:

  • 在 a.github.io/a.html 中创建 iframe 指向 b.github.io/b.html (页面会将自身的 window.name 附在 iframe 上)
  • 给 a.github.io/a.html 添加监听 iframe 的 onload 事件,在该事件中将 iframe 的 src 设置为本地域的代理文件(代理文件和a.html处于同一域下,可以相互通信),同时可以传出 iframe 的 name 值
  • 获取数据后销毁 iframe,释放内存,同时也保证了安全

window.name 的优势在于巧妙地绕过了浏览器的跨域访问限制,但同时它又是安全操作。

location.hash + iframe

location.hash就是锚点值,又称为片段标识符(fragment identifier),指的是URL的#号后面的部分。

如果只是改变片段标识符,页面不会重新刷新 ,所以可以利用hash值来进行数据的传递,当然数据量是有限的。

location.hash实现跨域核心

1.假设 github.io 域名下 a.html 和 mouyuming.eu 域名下 b.html 存在跨域请求,那么利用 location.hash 的一个解决方案如下:

  • a.html 页面中创建一个隐藏的 iframe, src 指向 b.html,其中 src 中可以通过 hash 传入参数给 b.html
  • b.html 页面在处理完传入的 hash 后通过修改 a.html 的 hash 值达到将数据传送给 a.html 的目的
  • a.html 页面添加一个定时器,每隔一定时间判断自身的 location.hash 是否变化,以此响应处理

以上步骤中需要注意第二点:如何在 iframe 页面中修改 父亲页面的 hash 值。由于在 IE 和 Chrome 下,两个不同域的页面是不允许 parent.location.hash 这样赋值的,所以对于这种情况,我们需要在父亲页面域名下添加另一个页面来实现跨域请求,具体如下:

  • 假设 a.html 中 iframe 引入了 b.html, 数据需要在这两个页面之间传递,且 c.html 是一个与 a.html 同源的页面
  • a.html 通过 iframe 将数据通过 hash 传给 b.html
  • b.html 通过 iframe 将数据通过 hash 传给 c.html
  • c.html 通过 parent.parent.location.hash 设置 a.html 的 hash 达到传递数据的目的

2.location.bash 方法的优缺点

优点在于可以解决域名完全不同的跨域请求,并且可以实现双向通讯

而缺点则包括以下几点:

  • 利用这种方法传递的数据量受到 url 大小的限制,传递数据类型有限
  • 由于数据直接暴露在 url 中则存在安全问题
  • 若浏览器不支持 onhashchange 事件,则需要通过轮训来获知 url 的变化
  • 有些浏览器会在 hash 变化时产生历史记录,因此可能影响用户体验

window.postMessage

HTML5 为了跨域问题,引入了一个全新的 API:跨文档通信 API(Cross-document messaging)。这个 API 为 window 对象新增了一个 window.postMessage 方法,允许跨窗口通信,不论这两个窗口是否同源。

详细方法见MDN

图所示为postMessage的兼容性:

1.语法

  • 格式:otherWindow.postMessage(message, targetOrigin, [transfer]);

  • 参数:
    otherWindow | 其他窗口的一个引用,比如iframe的contentWindow属性、执行window.open返回的窗口对象、或者是命名过或数值索引的window.frames。

    message | 将要发送到其他 window的数据;是具体的信息内容。

    targetOrigin | 接收消息的窗口的源(origin),即"协议 + 域名 + 端口"。也可以设为*,表示不限制域名,向所有窗口发送。

如果你明确的知道消息应该发送到哪个窗口,那么请始终提供一个有确切值的targetOrigin,而不是*。不提供确切的目标将导致数据泄露到任何对数据感兴趣的恶意站点。

2.父窗口和子窗口都可以通过message事件,监听对方的消息。

message 的属性有:

data | 从其他 window 中传递过来的对象。(消息内容)

origin | 调用 postMessage 时消息发送方窗口的 origin。(消息发向的网址)

source | 对发送消息的窗口对象的引用。(发送消息的窗口)

//发送页面
<body>
<iframe id="myIframe" src="http://localhost:3000/user/reg"></iframe>
<input type="button" value="点我" onclick="test()">
<script>
   function test() {
      let frm = document.getElementById("myIframe");
      frm.contentWindow.postMessage("跨域请求信息","http://localhost:3000");
    }
</script>
</body>

// 接收页面
<script>
    //接收信息页面 http://localhost:3000/parent.html
    window.addEventListener("message",function (e) {
       console.log(e.data);
    },false);
</script>
复制代码

其他方式跨域

NodeJS中间件代理跨域、nginx反向代理中设置、flash等第三方插件等

什么是反向代理
反向代理(Reverse Proxy)是指通过代理服务器来接收互联网上的连接请求,然后将请求转发给内部网络上的服务器,并把从服务器上得到的结果返回给互联网上请求连接的客户端,此时代理服务器对外就表现为一个反向代理服务器。

等我会了这几个再填这三种方式的坑

本文参考链接

iFrame跨域的方式

跨域资源共享的10种方式

前端跨域的几种方式(超详细,值得收藏)

window.postMessage MDN