弹窗和 window 的方法
弹窗(popup)是向用户显示其他文档的最古老的方法之一。
- 阻止弹窗
- 如果弹窗是在用户触发的事件处理程序(如
onclick
)之外调用的,大多数浏览器都会阻止此类弹窗。
- 如果弹窗是在用户触发的事件处理程序(如
// 弹窗被阻止
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. 包含最小功能集的新窗口,来看看哪些功能是浏览器允许禁用的:
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);
添加正常的定位选项和合理的 width
、height
、left
和 top
坐标:
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
调用会返回对新窗口的引用。它可以用来操纵弹窗的属性,更改位置,甚至更多操作。
let newWin = window.open("about:blank", "hello", "width=200,height=200");
newWin.document.write("Hello, world!");
其加载完成后,修改其中的内容:
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
,则处于安全性考虑,这两个窗口不能访问彼此的内容
- 从弹窗访问窗口
- 弹窗也可以使用
window.opener
来访问 opener 窗口。除了弹窗之外,对其他所有窗口来说,window.opener
均为null
。
- 弹窗也可以使用
let newWin = window.open("about:blank", "hello", "width=200,height=200");
newWin.document.write(
"<script>window.opener.document.body.innerHTML = 'Test'<\/script>"
);
- 关闭弹窗
- 关闭一个窗口:
win.close()
。 - 检查一个窗口是否被关闭:
win.closed
。 - 从技术上讲,
close()
方法可用于任何window
,但是如果window
不是通过window.open()
创建的,那么大多数浏览器都会忽略window.close()
。因此,close()
只对弹窗起作用。 - 如果窗口被关闭了,那么
closed
属性则为true
。这对于检查弹窗(或主窗口)是否仍处于打开状态很有用。用户可以随时关闭它,我们的代码应该考虑到这种可能性。
- 关闭一个窗口:
let newWindow = open('/', 'example', 'width=300,height=300');
newWindow.onload = function() {
newWindow.close();
alert(newWindow.closed); // true
};
- 移动和调整大小
- 有一些方法可以移动一个窗口,或者调整一个窗口的大小:
- 将窗口相对于当前位置向右移动
x
像素,并向下移动y
像素。允许负值(向上/向左移动)。win.moveBy(x,y)
- 将窗口移动到屏幕上的坐标
(x,y)
处。win.moveTo(x,y)
- 根据给定的相对于当前大小的
width/height
调整窗口大小。允许负值。win.resizeBy(width,height)
- 将窗口调整为给定的大小。
win.resizeTo(width,height)
- 还有
window.onresize
事件。
- 将窗口相对于当前位置向右移动
- 仅对于弹窗:为了防止滥用,浏览器通常会阻止这些方法。它们仅在我们打开的,没有其他选项卡的弹窗中能够可靠地工作。
- 没有最小化/最大化:JavaScript 无法最小化或者最大化一个窗口。这些操作系统级别的功能对于前端开发者而言是隐藏的。移动或者调整大小的方法不适用于最小化/最大化的窗口。
- 有一些方法可以移动一个窗口,或者调整一个窗口的大小:
- 滚动窗口
- 相对于当前位置,将窗口向右滚动
x
像素,并向下滚动y
像素。允许负值。win.scrollBy(x,y)
- 将窗口滚动到给定坐标
(x,y)
。win.scrollTo(x,y)
- 滚动窗口,使
elem
显示在elem.scrollIntoView(false)
的顶部(默认)或底部。elem.scrollIntoView(top = true)
- 这里也有
window.onscroll
事件。
- 相对于当前位置,将窗口向右滚动
- 弹窗的聚焦/失焦
- 从理论上讲,使用
window.focus()
和window.blur()
方法可以使窗口获得或失去焦点。此外,这里还有focus/blur
事件,可以捕获到访问者聚焦到一个窗口和切换到其他地方的时刻。
- 从理论上讲,使用
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
中的邮件。所以,“同源”策略的目的是保护用户免遭信息盗窃。
- 同源
- 如果两个 URL 具有相同的协议,域和端口,则称它们是“同源”的。
- “同源”策略规定:
- 如果我们有对另外一个窗口(例如,一个使用
window.open
创建的弹窗,或者一个窗口中的 iframe)的引用,并且该窗口是同源的,那么我们就具有对该窗口的全部访问权限。 - 否则,如果该窗口不是同源的,那么我们就无法访问该窗口中的内容:变量,文档,任何东西。唯一的例外是
location
:我们可以修改它(进而重定向用户)。但是我们无法读取location
(因此,我们无法看到用户当前所处的位置,也就不会泄漏任何信息)。
- 如果我们有对另外一个窗口(例如,一个使用
- 实例:iframe
- 一个
<iframe>
标签承载了一个单独的嵌入的窗口,它具有自己的document
和window
。 - 可以使用以下属性访问它们:
iframe.contentWindow
来获取<iframe>
中的 window。iframe.contentDocument
来获取<iframe>
中的 document,是iframe.contentWindow.document
的简写形式。 来自另一个源的<iframe>
进行读取和写入:
- 一个
<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.com
,peter.site.com
和 site.com
(它们共同的二级域是 site.com
),我们可以使浏览器忽略该差异,使得它们可以被作为“同源”的来对待,以便进行跨窗口通信。 2. 为了做到这一点,每个这样的窗口都应该执行这行代码:document
.domain
='site.com'
;
4. Iframe:错误文档陷阱 1. 当一个 iframe 来自同一个源时,我们可能会访问其 document
,但是这里有一个陷阱。它与跨源无关。 2. 在创建 iframe 后,iframe 会立即就拥有了一个文档。但是该文档不同于加载到其中的文档!
<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
中进行检查,以更早地捕获该时刻:
<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>
- 集合:window.frames
- 获取
<iframe>
的 window 对象的另一个方式是从命名集合window.frames
中获取:- 通过索引获取:
window.frames[0]
—— 文档中的第一个 iframe 的 window 对象。 - 通过名称获取:
window.frames.iframeName
—— 获取name="iframeName"
的 iframe 的 window 对象。
- 通过索引获取:
- 一个 iframe 内可能嵌套了其他的 iframe。相应的
window
对象会形成一个层次结构(hierarchy)。 - 可以通过以下方式获取:
window.frames
—— “子”窗口的集合(用于嵌套的 iframe)。window.parent
—— 对“父”(外部)窗口的引用。window.top
—— 对最顶级父窗口的引用。
- 获取
- “sandbox” iframe 特性
sandbox
特性(attribute)允许在<iframe>
中禁止某些特定行为,以防止其执行不被信任的代码。它通过将 iframe 视为非同源的,或者应用其他限制来实现 iframe 的“沙盒化”。- 对于
<iframe sandbox src="...">
,有一个应用于其上的默认的限制集。但是,我们可以通过提供一个以空格分隔的限制列表作为特性的值,来放宽这些限制,该列表中的各项为不应该应用于这个 iframe 的限制,例如:<iframe sandbox="allow-forms allow-popups">
。换句话说,一个空的"sandbox"
特性会施加最严格的限制,但是我们用一个以空格分隔的列表,列出要移除的限制。 - 以下是限制的列表:
allow-same-origin
:默认情况下,"sandbox"
会为 iframe 强制实施“不同来源”的策略。换句话说,它使浏览器将iframe
视为来自另一个源,即使其src
指向的是同一个网站也是如此。具有所有隐含的脚本限制。此选项会移除这些限制。allow-top-navigation
:允许iframe
更改parent.location
。allow-forms
:允许在iframe
中提交表单。allow-scripts
:允许在iframe
中运行脚本。allow-popups
:允许在iframe
中使用window.open
打开弹窗。- 查看 官方手册 获取更多内容。
"sandbox"
特性的目的仅是 添加更多 限制。它无法移除这些限制。尤其是,如果 iframe 来自其他源,则无法放宽同源策略。
- 跨窗口通信
postMessage
接口允许窗口之间相互通信,无论它们来自什么源。- postMessage
- 想要发送消息的窗口需要调用接收窗口的 postMessage 方法。换句话说,如果我们想把消息发送给
win
,我们应该调用win.postMessage(data, targetOrigin)
。 data
:要发送的数据。可以是任何对象,数据会被通过使用“结构化序列化算法(structured serialization algorithm)”进行克隆。IE 浏览器只支持字符串,因此我们需要对复杂的对象调用JSON.stringify
方法进行处理,以支持该浏览器。targetOrigin
:指定目标窗口的源,以便只有来自给定的源的窗口才能获得该消息。targetOrigin
是一种安全措施。请记住,如果目标窗口是非同源的,我们无法在发送方窗口读取它的location
。因此,我们无法确定当前在预期的窗口中打开的是哪个网站:用户随时可以导航离开,并且发送方窗口对此一无所知。- 指定
targetOrigin
可以确保窗口仅在当前仍处于正确的网站时接收数据。在有敏感数据时,这非常重要。
- 想要发送消息的窗口需要调用接收窗口的 postMessage 方法。换句话说,如果我们想把消息发送给
- onmessage
- 为了接收消息,目标窗口应该在
message
事件上有一个处理程序。当postMessage
被调用时触发该事件(并且targetOrigin
检查成功)。 - event 对象具有特殊属性:
data
: 从postMessage
传递来的数据。origin
:发送方的源,例如http://javascript.info
。source
:对发送方窗口的引用。如果我们想,我们可以立即source.postMessage(...)
回去。- 要为
message
事件分配处理程序,我们应该使用addEventListener
,简短的语法window.onmessage
不起作用。
- 为了接收消息,目标窗口应该在
点击劫持攻击
“点击劫持”攻击允许恶意页面 以用户的名义 点击“受害网站”。 许多网站都被黑客以这种方式攻击过,包括 Twitter、Facebook 和 Paypal 等许多网站。当然,它们都已经被修复了。
- 原理
- 访问者被恶意页面吸引。怎样吸引的不重要。
- 页面上有一个看起来无害的链接(例如:“变得富有”或者“点我,超好玩!”)。
- 恶意页面在该链接上方放置了一个透明的
<iframe>
,其src
来自于 facebook.com,这使得“点赞”按钮恰好位于该链接上面。这通常是通过z-index
实现的。 - 用户尝试点击该链接时,实际上点击的是“点赞”按钮。
- 示例
<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>
- 点击劫持是对点击事件,而非键盘事件
- 此攻击仅影响鼠标行为(或者类似的行为,例如在手机上的点击)。
- 键盘输入很难重定向。从技术上讲,我们可以用 iframe 的文本区域覆盖原有的文本区域实现攻击。因此,当访问者试图聚焦页面中的输入时,实际上聚焦的是 iframe 中的输入。
- 传统防御
- 最古老的防御措施是一段用于禁止在 frame 中打开页面的 JavaScript 代码(所谓的 “framebusting”)。
if (top != window) {
top.location = window.location;
}
- 阻止顶级导航
- 我们可以阻止因更改 beforeunload 事件处理程序中的
top.location
而引起的过渡(transition)。 - 顶级页面(从属于黑客)在
beforeunload
上设置了一个用于阻止的处理程序。
- 我们可以阻止因更改 beforeunload 事件处理程序中的
window.onbeforeunload = function() {
return false;
};
- Sandbox 特性
sandbox
特性的限制之一就是导航。沙箱化的 iframe 不能更改top.location
。- 但我们可以添加具有
sandbox="allow-scripts allow-forms"
的 iframe。从而放开限制,允许脚本和表单。但我们没添加allow-top-navigation
,因此更改top.location
是被禁止的。
<iframe sandbox="allow-scripts allow-forms" src="facebook.html"></iframe>
- X-Frame-Options
- 服务器端 header
X-Frame-Options
可以允许或禁止在 frame 中显示页面。 - 它必须被完全作为 HTTP-header 发送:如果浏览器在 HTML
<meta>
标签中找到它,则会忽略它。因此,<meta http-equiv="X-Frame-Options"...>
没有任何作用。 - 这个 header 可能包含 3 个值:
DENY
:始终禁止在 frame 中显示此页面。SAMEORIGIN
:允许在和父文档同源的 frame 中显示此页面。ALLOW-FROM domain
:允许在来自给定域的父文档的 frame 中显示此页面。
- 服务器端 header
- 显示禁用的功能
X-Frame-Options
有一个副作用。其他的网站即使有充分的理由也无法在 frame 中显示我们的页面。- 因此,还有其他解决方案……例如,我们可以用一个样式为
height: 100%; width: 100%;
的<div>
“覆盖”页面,这样它就能拦截所有点击。如果window == top
或者我们确定不需要保护时,再将该<div>
移除。
<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>
- Samesite cookie 特性
samesite
cookie 特性也可以阻止点击劫持攻击。- 具有
samesite
特性的 cookie 仅在网站是通过直接方式打开(而不是通过 frame 或其他方式)的情况下才发送到网站。更多细节请见 Cookie,document.cookie。
Set-Cookie: authorization=secret; samesite