Skip to content

弹窗和 window 的方法

弹窗(popup)是向用户显示其他文档的最古老的方法之一。

  1. 阻止弹窗
    1. 如果弹窗是在用户触发的事件处理程序(如 onclick)之外调用的,大多数浏览器都会阻止此类弹窗。
JavaScript
// 弹窗被阻止
window.open('https://javascript.info');

// 弹窗被允许
button.onclick = () => {
  window.open('https://javascript.info');
};

这种方式可以在某种程度上保护用户免受非必要的弹窗的影响,但是并没有完全阻止该功能。 2. window.open 1. 打开一个弹窗的语法是 window.open(url, name, params): 1. url:要在新窗口中加载的 URL。 2. name:新窗口的名称。每个窗口都有一个 window.name,在这里我们可以指定哪个窗口用于弹窗。如果已经有一个这样名字的窗口 —— 将在该窗口打开给定的 URL,否则会打开一个新窗口。 3. params:新窗口的配置字符串。它包括设置,用逗号分隔。参数之间不能有空格,例如:width=200,height=100。 4. params 的设置项--位置: - left/top(数字)—— 屏幕上窗口的左上角的坐标。这有一个限制:不能将新窗口置于屏幕外(offscreen)。 - width/height(数字)—— 新窗口的宽度和高度。宽度/高度的最小值是有限制的,因此不可能创建一个不可见的窗口。 5. params 的设置项--窗口功能: - menubar(yes/no)—— 显示或隐藏新窗口的浏览器菜单。 - toolbar(yes/no)—— 显示或隐藏新窗口的浏览器导航栏(后退,前进,重新加载等)。 - location(yes/no)—— 显示或隐藏新窗口的 URL 字段。Firefox 和 IE 浏览器不允许默认隐藏它。 - status(yes/no)—— 显示或隐藏状态栏。同样,大多数浏览器都强制显示它。 - resizable(yes/no)—— 允许禁用新窗口大小调整。不建议使用。 - scrollbars(yes/no)—— 允许禁用新窗口的滚动条。不建议使用。 3. 示例:一个最简窗口 1. 包含最小功能集的新窗口,来看看哪些功能是浏览器允许禁用的:

JavaScript
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
width=0,height=0,left=-1000,top=-1000`;

open('/', 'test', params);

添加正常的定位选项和合理的 widthheightleft 和 top 坐标:

JavaScript
let params = `scrollbars=no,resizable=no,status=no,location=no,toolbar=no,menubar=no,
width=600,height=300,left=100,top=100`;

open('/', 'test', params);

设置中的省略规则: - 如果 open 调用中没有第三个参数,或者它是空的,则使用默认的窗口参数。 - 如果这里有一个参数字符串,但是某些 yes/no 功能被省略了,那么被省略的功能则被默认值为 no。因此,如果你指定参数,请确保将所有必需的功能明确设置为 yes。 - 如果参数中没有 left/top,那么浏览器会尝试在最后打开的窗口附近打开一个新窗口。 - 如果没有 width/height,那么新窗口的大小将与上次打开的窗口大小相同。 4. 从窗口访问弹窗 1. open 调用会返回对新窗口的引用。它可以用来操纵弹窗的属性,更改位置,甚至更多操作。

JavaScript
let newWin = window.open("about:blank", "hello", "width=200,height=200");

newWin.document.write("Hello, world!");

其加载完成后,修改其中的内容:

JavaScript
let newWindow = open('/', 'example', 'width=300,height=300')
newWindow.focus();

alert(newWindow.location.href); // (*) about:blank,加载尚未开始

newWindow.onload = function() {
  let html = `<div style="font-size:30px">Welcome!</div>`;
  newWindow.document.body.insertAdjacentHTML('afterbegin', html);
};

同源策略:只有在窗口是同源的时,窗口才能自由访问彼此的内容(相同的协议://domain:port)。否则,例如,如果主窗口来自于 site.com,弹窗来自于 gmail.com,则处于安全性考虑,这两个窗口不能访问彼此的内容

  1. 从弹窗访问窗口
    1. 弹窗也可以使用 window.opener 来访问 opener 窗口。除了弹窗之外,对其他所有窗口来说,window.opener 均为 null
JavaScript
let newWin = window.open("about:blank", "hello", "width=200,height=200");

newWin.document.write(
  "<script>window.opener.document.body.innerHTML = 'Test'<\/script>"
);
  1. 关闭弹窗
    1. 关闭一个窗口:win.close()
    2. 检查一个窗口是否被关闭:win.closed
    3. 从技术上讲,close() 方法可用于任何 window,但是如果 window 不是通过 window.open() 创建的,那么大多数浏览器都会忽略 window.close()。因此,close() 只对弹窗起作用。
    4. 如果窗口被关闭了,那么 closed 属性则为 true。这对于检查弹窗(或主窗口)是否仍处于打开状态很有用。用户可以随时关闭它,我们的代码应该考虑到这种可能性。
JavaScript
let newWindow = open('/', 'example', 'width=300,height=300');

newWindow.onload = function() {
  newWindow.close();
  alert(newWindow.closed); // true
};
  1. 移动和调整大小
    1. 有一些方法可以移动一个窗口,或者调整一个窗口的大小:
      1. 将窗口相对于当前位置向右移动 x 像素,并向下移动 y 像素。允许负值(向上/向左移动)。win.moveBy(x,y)
      2. 将窗口移动到屏幕上的坐标 (x,y) 处。win.moveTo(x,y)
      3. 根据给定的相对于当前大小的 width/height 调整窗口大小。允许负值。win.resizeBy(width,height)
      4. 将窗口调整为给定的大小。win.resizeTo(width,height)
      5. 还有 window.onresize 事件。
    2. 仅对于弹窗:为了防止滥用,浏览器通常会阻止这些方法。它们仅在我们打开的,没有其他选项卡的弹窗中能够可靠地工作。
    3. 没有最小化/最大化:JavaScript 无法最小化或者最大化一个窗口。这些操作系统级别的功能对于前端开发者而言是隐藏的。移动或者调整大小的方法不适用于最小化/最大化的窗口。
  2. 滚动窗口
    1. 相对于当前位置,将窗口向右滚动 x 像素,并向下滚动 y 像素。允许负值。win.scrollBy(x,y)
    2. 将窗口滚动到给定坐标 (x,y)win.scrollTo(x,y)
    3. 滚动窗口,使 elem 显示在 elem.scrollIntoView(false) 的顶部(默认)或底部。elem.scrollIntoView(top = true)
    4. 这里也有 window.onscroll 事件。
  3. 弹窗的聚焦/失焦
    1. 从理论上讲,使用 window.focus() 和 window.blur() 方法可以使窗口获得或失去焦点。此外,这里还有 focus/blur 事件,可以捕获到访问者聚焦到一个窗口和切换到其他地方的时刻。
JavaScript
window.onblur = () => window.focus();

当用户尝试从窗口切换出去(window.onblur)时,这段代码又让窗口重新获得了焦点。目的是将用户“锁定”在 window 中。 因此,浏览器必须引入很多限制,以禁用此类代码并保护用户免受广告和恶意页面的侵害。具体则取决于浏览器。 例如,移动端浏览器通常会完全忽略 window.focus()。并且,当弹窗是在单独的选项卡而不是新窗口中打开时,也无法进行聚焦。 尽管如此,在某些情况下,此类调用确实有效且很有用。 例如:

  • 当我们打开一个弹窗时,在它上面执行 newWindow.focus() 是个好主意。以防万一,对于某些操作系统/浏览器组合(combination),它可以确保用户现在位于新窗口中。
  • 如果我们想要跟踪访问者何时在实际使用我们的 Web 应用程序,我们可以跟踪 window.onfocus/onblur。这使我们可以暂停/恢复页面活动和动画等。但是请注意,blur 事件意味着访问者从窗口切换了出来,但他们仍然可以观察到它。窗口处在背景中,但可能仍然是可见的。

跨窗口通信

同源(Same Origin)”策略限制了窗口(window)和 frame 之间的相互访问。 这个想法出于这样的考虑,如果一个用户有两个打开的页面:一个来自 john-smith.com,另一个是 gmail.com,那么用户将不希望 john-smith.com 的脚本可以读取 gmail.com 中的邮件。所以,“同源”策略的目的是保护用户免遭信息盗窃。

  1. 同源
    1. 如果两个 URL 具有相同的协议,域和端口,则称它们是“同源”的。
    2. “同源”策略规定:
      • 如果我们有对另外一个窗口(例如,一个使用 window.open 创建的弹窗,或者一个窗口中的 iframe)的引用,并且该窗口是同源的,那么我们就具有对该窗口的全部访问权限。
      • 否则,如果该窗口不是同源的,那么我们就无法访问该窗口中的内容:变量,文档,任何东西。唯一的例外是 location:我们可以修改它(进而重定向用户)。但是我们无法读取 location(因此,我们无法看到用户当前所处的位置,也就不会泄漏任何信息)。
  2. 实例:iframe
    1. 一个 <iframe> 标签承载了一个单独的嵌入的窗口,它具有自己的 document 和 window
    2. 可以使用以下属性访问它们:
      • iframe.contentWindow 来获取 <iframe> 中的 window。
      • iframe.contentDocument 来获取 <iframe> 中的 document,是 iframe.contentWindow.document 的简写形式。 来自另一个源的 <iframe> 进行读取和写入:
html
<iframe src="https://example.com" id="iframe"></iframe>

<script>
  iframe.onload = function() {
    // 我们可以获取对内部 window 的引用
    let iframeWindow = iframe.contentWindow; // OK
    try {
      // ...但是无法获取其中的文档
      let doc = iframe.contentDocument; // ERROR
    } catch(e) {
      alert(e); // Security Error(另一个源)
    }

    // 并且,我们也无法读取 iframe 中页面的 URL
    try {
      // 无法从 location 对象中读取 URL
      let href = iframe.contentWindow.location.href; // ERROR
    } catch(e) {
      alert(e); // Security Error
    }

    // ...我们可以写入 location(所以,在 iframe 中加载了其他内容)!
    iframe.contentWindow.location = '/'; // OK

    iframe.onload = null; // 清空处理程序,在 location 更改后不要再运行它
  };
</script>

以上代码只能做: - 通过 iframe.contentWindow 获取对内部 window 的引用 —— 这是被允许的。 - 对 location 进行写入 3. 子域上的 window:document.domain 1. 如果窗口的二级域相同,例如 john.site.competer.site.com 和 site.com(它们共同的二级域是 site.com),我们可以使浏览器忽略该差异,使得它们可以被作为“同源”的来对待,以便进行跨窗口通信。 2. 为了做到这一点,每个这样的窗口都应该执行这行代码:document.domain ='site.com'; 4. Iframe:错误文档陷阱 1. 当一个 iframe 来自同一个源时,我们可能会访问其 document,但是这里有一个陷阱。它与跨源无关。 2. 在创建 iframe 后,iframe 会立即就拥有了一个文档。但是该文档不同于加载到其中的文档!

html
<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;
  iframe.onload = function() {
    let newDoc = iframe.contentDocument;
    // 加载的文档与初始的文档不同!
    alert(oldDoc == newDoc); // false
  };
</script>

不应该对尚未加载完成的 iframe 的文档进行处理,因为那是 错误的文档。如果我们在其上设置了任何事件处理程序,它们将会被忽略。 正确的文档在 iframe.onload 触发时肯定就位了。但是,只有在整个 iframe 和它所有资源都加载完成时,iframe.onload 才会触发。 尝试通过在 setInterval 中进行检查,以更早地捕获该时刻:

html
<iframe src="/" id="iframe"></iframe>

<script>
  let oldDoc = iframe.contentDocument;

  // 每 100ms 检查一次文档是否为新文档
  let timer = setInterval(() => {
    let newDoc = iframe.contentDocument;
    if (newDoc == oldDoc) return;

    alert("New document is here!");

    clearInterval(timer); // 取消 setInterval,不再需要它做任何事儿
  }, 100);
</script>
  1. 集合:window.frames
    1. 获取 <iframe> 的 window 对象的另一个方式是从命名集合 window.frames 中获取:
      • 通过索引获取:window.frames[0] —— 文档中的第一个 iframe 的 window 对象。
      • 通过名称获取:window.frames.iframeName —— 获取 name="iframeName" 的 iframe 的 window 对象。
    2. 一个 iframe 内可能嵌套了其他的 iframe。相应的 window 对象会形成一个层次结构(hierarchy)。
    3. 可以通过以下方式获取:
      • window.frames —— “子”窗口的集合(用于嵌套的 iframe)。
      • window.parent —— 对“父”(外部)窗口的引用。
      • window.top —— 对最顶级父窗口的引用。
  2. “sandbox” iframe 特性
    1. sandbox 特性(attribute)允许在 <iframe> 中禁止某些特定行为,以防止其执行不被信任的代码。它通过将 iframe 视为非同源的,或者应用其他限制来实现 iframe 的“沙盒化”。
    2. 对于 <iframe sandbox src="...">,有一个应用于其上的默认的限制集。但是,我们可以通过提供一个以空格分隔的限制列表作为特性的值,来放宽这些限制,该列表中的各项为不应该应用于这个 iframe 的限制,例如:<iframe sandbox="allow-forms allow-popups">。换句话说,一个空的 "sandbox" 特性会施加最严格的限制,但是我们用一个以空格分隔的列表,列出要移除的限制。
    3. 以下是限制的列表:
      1. allow-same-origin:默认情况下,"sandbox" 会为 iframe 强制实施“不同来源”的策略。换句话说,它使浏览器将 iframe 视为来自另一个源,即使其 src 指向的是同一个网站也是如此。具有所有隐含的脚本限制。此选项会移除这些限制。
      2. allow-top-navigation:允许 iframe 更改 parent.location
      3. allow-forms:允许在 iframe 中提交表单。
      4. allow-scripts:允许在 iframe 中运行脚本。
      5. allow-popups:允许在 iframe 中使用 window.open 打开弹窗。
      6. 查看 官方手册 获取更多内容。
    4. "sandbox" 特性的目的仅是 添加更多 限制。它无法移除这些限制。尤其是,如果 iframe 来自其他源,则无法放宽同源策略。
  3. 跨窗口通信
    1. postMessage 接口允许窗口之间相互通信,无论它们来自什么源。
    2. postMessage
      1. 想要发送消息的窗口需要调用接收窗口的 postMessage 方法。换句话说,如果我们想把消息发送给 win,我们应该调用 win.postMessage(data, targetOrigin)
      2. data:要发送的数据。可以是任何对象,数据会被通过使用“结构化序列化算法(structured serialization algorithm)”进行克隆。IE 浏览器只支持字符串,因此我们需要对复杂的对象调用 JSON.stringify 方法进行处理,以支持该浏览器。
      3. targetOrigin:指定目标窗口的源,以便只有来自给定的源的窗口才能获得该消息。
      4. targetOrigin 是一种安全措施。请记住,如果目标窗口是非同源的,我们无法在发送方窗口读取它的 location。因此,我们无法确定当前在预期的窗口中打开的是哪个网站:用户随时可以导航离开,并且发送方窗口对此一无所知。
      5. 指定 targetOrigin 可以确保窗口仅在当前仍处于正确的网站时接收数据。在有敏感数据时,这非常重要。
    3. onmessage
      1. 为了接收消息,目标窗口应该在 message 事件上有一个处理程序。当 postMessage 被调用时触发该事件(并且 targetOrigin 检查成功)。
      2. event 对象具有特殊属性:
        1. data: 从 postMessage 传递来的数据。
        2. origin:发送方的源,例如 http://javascript.info
        3. source:对发送方窗口的引用。如果我们想,我们可以立即 source.postMessage(...) 回去。
        4. 要为 message 事件分配处理程序,我们应该使用 addEventListener,简短的语法 window.onmessage 不起作用。

点击劫持攻击

“点击劫持”攻击允许恶意页面 以用户的名义 点击“受害网站”。 许多网站都被黑客以这种方式攻击过,包括 Twitter、Facebook 和 Paypal 等许多网站。当然,它们都已经被修复了。

  1. 原理
    1. 访问者被恶意页面吸引。怎样吸引的不重要。
    2. 页面上有一个看起来无害的链接(例如:“变得富有”或者“点我,超好玩!”)。
    3. 恶意页面在该链接上方放置了一个透明的 <iframe>,其 src 来自于 facebook.com,这使得“点赞”按钮恰好位于该链接上面。这通常是通过 z-index 实现的。
    4. 用户尝试点击该链接时,实际上点击的是“点赞”按钮。
  2. 示例
html
<style>
iframe { /* 来自受害网站的 iframe */
  width: 400px;
  height: 100px;
  position: absolute;
  top:0; left:-20px;
  opacity: 0.5; /* 在实际中为 opacity:0 */
  z-index: 1;
}
</style>

<div>点击即可变得富有:</div>

<!-- 来自受害网站的 url -->
<iframe src="/clickjacking/facebook.html"></iframe>

<button>点这里!</button>

<div>……你很酷(我实际上是一名帅气的黑客)!</div>
  1. 点击劫持是对点击事件,而非键盘事件
    1. 此攻击仅影响鼠标行为(或者类似的行为,例如在手机上的点击)。
    2. 键盘输入很难重定向。从技术上讲,我们可以用 iframe 的文本区域覆盖原有的文本区域实现攻击。因此,当访问者试图聚焦页面中的输入时,实际上聚焦的是 iframe 中的输入。
  2. 传统防御
    1. 最古老的防御措施是一段用于禁止在 frame 中打开页面的 JavaScript 代码(所谓的 “framebusting”)。
JavaScript
if (top != window) {
  top.location = window.location;
}
  1. 阻止顶级导航
    1. 我们可以阻止因更改 beforeunload 事件处理程序中的 top.location 而引起的过渡(transition)。
    2. 顶级页面(从属于黑客)在 beforeunload 上设置了一个用于阻止的处理程序。
JavaScript
window.onbeforeunload = function() {
  return false;
};
  1. Sandbox 特性
    1. sandbox 特性的限制之一就是导航。沙箱化的 iframe 不能更改 top.location
    2. 但我们可以添加具有 sandbox="allow-scripts allow-forms" 的 iframe。从而放开限制,允许脚本和表单。但我们没添加 allow-top-navigation,因此更改 top.location 是被禁止的。
html
<iframe sandbox="allow-scripts allow-forms" src="facebook.html"></iframe>
  1. X-Frame-Options
    1. 服务器端 header X-Frame-Options 可以允许或禁止在 frame 中显示页面。
    2. 它必须被完全作为 HTTP-header 发送:如果浏览器在 HTML <meta> 标签中找到它,则会忽略它。因此,<meta http-equiv="X-Frame-Options"...> 没有任何作用。
    3. 这个 header 可能包含 3 个值:
      1. DENY:始终禁止在 frame 中显示此页面。
      2. SAMEORIGIN:允许在和父文档同源的 frame 中显示此页面。
      3. ALLOW-FROM domain:允许在来自给定域的父文档的 frame 中显示此页面。
  2. 显示禁用的功能
    1. X-Frame-Options 有一个副作用。其他的网站即使有充分的理由也无法在 frame 中显示我们的页面。
    2. 因此,还有其他解决方案……例如,我们可以用一个样式为 height: 100%; width: 100%; 的 <div> “覆盖”页面,这样它就能拦截所有点击。如果 window == top 或者我们确定不需要保护时,再将该 <div> 移除。
html
<style>
  #protector {
    height: 100%;
    width: 100%;
    position: absolute;
    left: 0;
    top: 0;
    z-index: 99999999;
  }
</style>

<div id="protector">
  <a href="/" target="_blank">前往网站</a>
</div>

<script>
  // 如果顶级窗口来自其他源,这里则会出现一个 error
  // 但是在本例中没有问题
  if (top.document.domain == document.domain) {
    protector.remove();
  }
</script>
  1. Samesite cookie 特性
    1. samesite cookie 特性也可以阻止点击劫持攻击。
    2. 具有 samesite 特性的 cookie 仅在网站是通过直接方式打开(而不是通过 frame 或其他方式)的情况下才发送到网站。更多细节请见 Cookie,document.cookie
JavaScript
Set-Cookie: authorization=secret; samesite