避免浏览器弹出窗口拦截器

221

我正在纯JavaScript中开发OAuth认证流程,想在弹出窗口中显示“授权访问”窗口,但是该窗口被阻止了。

如何防止使用window.openwindow.showModalDialog创建的弹出窗口被不同浏览器的弹出窗口拦截器拦截?


13
即便这是可能的(我不确定),如果人们使用弹窗拦截器,你应该尊重它。大多数浏览器在网站试图打开弹窗时会显示消息,因此用户仍然可以选择查看。你可以在网站上加注说明,某些内容需要在弹窗中打开,用户应允许它以继续操作。 - Felix Kling
5
最佳实践如下: 1)成功地完成这个任务 2)关闭窗户,锁好门,畏缩不前,躲避愤怒的网络用户群体 3)悔改,删除弹出式广告,并尊重你的受众。 - Alex Mcp
Alex和Felix,我已经更新了问题。我不会将这些知识用于邪恶目的:)。谢谢! - Pablo Fernandez
13
我想补充一点,绕过弹出窗口拦截可能是为了提高用户体验。比如,我现在正在处理一个例子,我们使用基于ExtJS的Javascript应用程序,让用户可以使用PayPal支付。我们提供一个按钮,用户可以点击它,在新窗口中打开PayPal,但某些版本的IE会将其阻止为弹出窗口(即使只是按钮点击)。如果用户启用了弹出窗口,界面重新加载,作为一个JavaScript应用程序,我们会丢失窗口状态,他们必须重新开始。所以真正的问题是IE太傻了。 - NateDSaint
@PabloFernandez你解决了这个问题吗?我注意到你接受了dthorpe的答案,但想知道这对你有用还是OAuth弹出窗口仍然被拦截? - Drewdavid
1
@FelixKling 是的,这是可能的。浏览器制造商已经考虑过这个问题。只要有用户意图(由用户点击链接或按钮表示),打开弹出窗口就可以了。弹出窗口阻止程序应该尊重用户的意愿。如果 IE 的弹出窗口阻止程序没有起作用,那么弹出窗口阻止程序就是有问题的。用户使用弹出窗口阻止程序是为了防止脚本随意打开弹出窗口,而不是阻止他们自己试图打开的弹出窗口(通过单击按钮或链接)。 - Stijn de Witt
12个回答

342

一般规则是,如果从非直接用户操作调用的 JavaScript 中调用 window.open 或类似函数,弹出窗口拦截器将会启动。也就是说,你可以在响应按钮单击时调用 window.open 而不会被弹出窗口拦截器拦截,但如果将相同的代码放置在计时器事件中,则会被拦截。调用链的深度也是一个因素 - 一些旧版浏览器只查看直接调用者,而新版浏览器可以向后跟踪一点,以查看调用者的调用者是否是鼠标点击等。尽可能保持浅层次,以避免弹出窗口拦截器。


2
有趣的是,通过绑定到选择元素的更改事件启动的弹出窗口将被阻止(在Chrome中,而不是FF),即使该事件是由直接用户操作(如单击)引发的。虽然如果绑定到输入,则允许它们。奇怪的事情。 - ccnokes
8
没人说浏览器是一致的。 :P - dthorpe
17
经过实验,我了解到堆栈深度与弹出窗口拦截器无关。它实际上检查的是用户操作后是否在1秒内调用了window.open。在Chrome 46和Firefox 42中进行了测试。 - Mesqalito
1秒的超时时间在两个浏览器中都可以被规避,使用@t-j-crowder在此页面上的答案可以无限期地使用时间。让您通过ajax调用的URL是一些使用sleep()等待一段时间才返回响应的php(或其他语言)代码。当浏览器“加载”页面时,它将挂起..然后在接收到响应时触发弹出窗口。但是,在Chrome中,您必须在ajax()之前添加一个alert()才能使这个技巧起作用。 - fanfare

233
根据Jason Sebring非常实用的提示,以及这里那里所涵盖的内容,我找到了适用于我的情况的完美解决方案:
使用JavaScript代码片段的伪代码:
  1. immediately create a blank popup on user action

     var importantStuff = window.open('', '_blank');
    

    (Enrich the call to window.open with whatever additional options you need.)

    Optional: add some "waiting" info message. Examples:

    a) An external HTML page: replace the above line with

     var importantStuff = window.open('http://example.com/waiting.html', '_blank');
    

    b) Text: add the following line below the above one:

     importantStuff.document.write('Loading preview...');
    
  2. fill it with content when ready (when the AJAX call is returned, for instance)

     importantStuff.location.href = 'https://example.com/finally.html';
    

    Alternatively, you could close the window here if you don't need it after all (if ajax request fails, for example - thanks to @Goose for the comment):

     importantStuff.close();
    

我实际上使用这个解决方案进行mailto重定向,它可以在我所有的浏览器(Windows 7,Android)上工作。顺便说一句,_blank 部分有助于在移动设备上实现mailto重定向。


1
那么如果不是来自浏览器用户操作,你该怎么办呢?以我的例子为例,在用户对服务器进行身份验证后,必须打开一个新标签页。 - Don Cheadle
7
无论你想做什么,都有比弹出窗口更好的方式。这是个奇怪的概括。客户希望在认证页面保持打开状态,并在新标签页中打开认证后的新站点/落地页。虽然这不是我理解中的卓越用户体验……但似乎很合理。 - Don Cheadle
只需按照答案的第二步进行操作,使用用户将要访问的任何“已验证”页面即可。为什么这样做不起作用? - Swiss Mister
1
我真的很欣赏那个解决方案。它运行得非常好。改进这个解决方案的一种方法是在加载页面上添加一个脚本,几秒钟后自动关闭。对于我的用例来说,这是更好的行为。谢谢! - Anderson Marques
4
如果 Ajax 请求失败,请调用 importantStuff.close 关闭新标签页,并在原始页面中提供警告提示。 - Goose
显示剩余6条评论

30

作为一个良好的实践,我认为检测弹出窗口是否被阻止并在必要时采取行动是一个不错的主意。您需要知道 window.open 有一个返回值,如果操作失败,该值可能为 null。例如,在下面的代码中:

function pop(url,w,h) {
    n=window.open(url,'_blank','toolbar=0,location=0,directories=0,status=1,menubar=0,titlebar=0,scrollbars=1,resizable=1,width='+w+',height='+h);
    if(n==null) {
        return true;
    }
    return false;
}

如果弹出窗口被阻止,window.open将返回null。因此,该函数将返回false。

例如,假设直接从带有target="_blank"的任何链接调用此函数:如果成功打开了弹出窗口,则返回false将阻止链接操作,否则如果弹出窗口被阻止,则返回true将执行默认行为(打开新的_blank窗口)并继续。

<a href="http://whatever.com" target="_blank" onclick='return pop("http://whatever.com",300,200);' >

这样,如果弹出窗口可以使用,您将会看到一个弹出窗口,否则会打开一个_blank窗口。

如果弹出窗口无法打开,您可以:

  • 像示例中一样打开一个空白窗口,然后继续操作。
  • 打开一个假的弹出窗口(在页面内部使用iframe)。
  • 通知用户(“请允许此站点的弹出窗口”)。
  • 打开一个空白窗口并向用户提供信息等。

谢谢。工作了! - Bcktr
除了上面提到的替代方法,我还考虑使用window.confirm()来再次获得用户同意,以便在直接用户点击时打开弹出窗口。但令人惊讶的是,它没有起作用 :( - mdehghani

24

除了瑞士先生的帖子之外,在我的情况下,window.open 是在一个 promise 内部启动的,这使得弹出窗口拦截器被激活。我的解决方案如下:

在 Angular 中:

$scope.gotClick = function(){

  var myNewTab = browserService.openNewTab();
  someService.getUrl().then(
    function(res){
        browserService.updateTabLocation(res.url, myNewTab);

    }
  );
};

浏览器服务:

this.openNewTab = function(){
     var newTabWindow = $window.open();
     return newTabWindow;
}

this.updateTabLocation = function(tabLocation, tab) {
     if(!tabLocation){
       tab.close();
     }
     tab.location.href = tabLocation;
}

这是使用承诺响应而不触发弹出窗口拦截器打开新标签页的方法。


2
这就是我的解决方案!我创建了一个变量来存储打开的标签页,然后在数据加载完成后填充URL。谢谢。const tab = window.open(); observable.subscribe(dataUrl => tab.location.href = dataUrl); - jonas
1
如果您启用了弹出窗口拦截器,这将无法解决弹出窗口拦截器的问题... - Alejandro Vales
@Alejandro Vales Jonas的解决方案仅在代码从onClick事件或任何用户交互中运行时有效。当您尝试在promise、timeout、subscribe等内部通过windows.open打开一个新标签页时,浏览器会认为这是对用户的操纵。因此,解决方案是在进入promise之前创建一个实例,在其中只需像Jonas所做的那样更改href即可。 - David
@David 非常感谢!!!我没有看到答案中的.then,所以我很困惑 :/ 对不起... - Alejandro Vales
@David 这太棒了!运行得非常好。如果我可以再麻烦您一个问题——您知道如何使它在Edge中工作吗?指向正确资源的提示将非常有帮助... - dzenesiz

8

来自Google的oauth JavaScript API:

http://code.google.com/p/google-api-javascript-client/wiki/Authentication

请看下面这个部分:

设置验证

客户端OAuth 2.0的实现使用弹出窗口提示用户登录和授权应用程序。首次调用gapi.auth.authorize可能会触发弹出拦截器,因为它间接地打开弹出窗口。为了防止弹出拦截器在身份验证调用上触发,当客户端加载时调用gapi.auth.init(callback)。当库准备好进行身份验证调用时,将执行提供的回调。

我猜这个与上面的真实答案有关,因为它解释了如果有立即响应,它不会触发弹出窗口警报。 "gapi.auth.init"使api立即运行。

实际应用

我使用npm上的node passport和各种护照包制作了一个开放源代码的身份验证微服务。我使用标准的重定向方法向第三方提供了一个重定向URL。这是编程方式的,所以我可以在不同的位置重定向回来,如果登录/注册并在特定页面上。

github.com/sebringj/athu

passportjs.org


7

我尝试了多个解决方案,但这是唯一一个在所有浏览器中都实际起作用的。

let newTab = window.open(); newTab.location.href = url;


3
对于启用弹窗拦截器的人来说,这并不起作用。 - Alejandro Vales
什么是 url?它没有被赋值。 - shinriyo
@shinriyo url 是你想要访问的页面的链接,例如 http://example.com - pomobc
1
@AlejandroVales 我已启用弹出窗口拦截器并且它有效。问题在于window.open()无法以编程方式打开新窗口,如果需要,该答案是其中之一。使用newTab变量,我们引用window.open(),稍后可以调用它,在其中插入所需的url,在我的情况下是某些blob的url,并且新标签将在输入的url上打开。 - stanimirsp
1
是的,对于启用了广告拦截器的人来说它不起作用。这是我们必须避免的问题。 - Mubasher Ali

7

我的使用场景: 在我的React应用中,用户点击后会向后端进行API调用。根据响应,在新标签页中打开,将API响应作为参数添加到新标签页的URL中(在同一域)。

我使用情况中唯一需要注意的是,这个API响应需要1秒以上才能接收到。因此,在打开新标签页时,如果弹出窗口拦截器处于活动状态,则会弹出。

为了规避上述问题,以下是示例代码:

var new_tab=window.open()
axios.get('http://backend-api').then(response=>{
    const url="http://someurl"+"?response"
    new_tab.location.href=url;
}).catch(error=>{
    //catch error
})

摘要:创建一个空选项卡(如上图第1行),当API调用完成后,您可以使用URL填充选项卡并跳过弹出窗口拦截器。


4

@在这里 我发现它在所有浏览器中都能正常工作

window.open(URL) || window.location.assign(URL)

4

只需使用 window.location.href = 您的Url 或一个带有 target='_blank' 的url

window.open() 问题太多了,如果距离用户操作超过一秒钟,许多浏览器会将其视为弹出窗口并阻止它。


window.location.href = yourUrl 在这里起作用 - niltonxp

0

我不想在回调函数成功返回之前创建新页面,所以我这样做来模拟用户点击:

function submitAndRedirect {
  apiCall.then(({ redirect }) => {
      const a = document.createElement('a');
      a.href = redirect;
      a.target = '_blank';
      document.body.appendChild(a);
      a.click();
      document.body.removeChild(a);
  });
}

4
在Chrome 62浏览器中无法使用。弹出窗口(页面)被阻止。 - s.ermakovich
1
是的,你说得对 - 我也发现这个不一致。最终我决定将我的API调用变成同步的。 - user3479425

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接