2

我创建了一个独立的 Google Apps Script Web 应用程序,我试图将它嵌入到新的 Google 协作平台中。当我登录用于创建 Apps 脚本项目的帐户时,它可以正常工作。但是,如果我登录到另一个尚未授权 Web 应用程序的帐户,Google 协作平台页面会加载,但带有嵌入式 Apps 脚本项目的 iFrame 无法正确加载。

相反,iFrame 显示“accounts.google.com 拒绝连接”,控制台显示“拒绝显示” https://accounts.google.com/ServiceLogin?passive=1209600&continue=https%3A%2F%2Fscript.google.com %2Fmacros%2Fs%2FAKfycbzizTNkflSXZbKSF8TTxTR5QoF4LAhPPuSq-1juFdIOdL_IlFM%2Fexec&followup=https%3A%2F%2Fscript.google.com%2Fmacros%2Fs%2FAKfycbzizTNkflSXZbKSF8TTxTR5QoF4LAhPPuSq-1juFdIOdL_IlFM%2Fexec ' in a frame because it set 'X-Frame-Options' to 'deny' 。”

据我了解,新用户无权使用我的 Apps Script Web App,这会触发授权流程。但是,当授权流程通过加载 Google 登录页面(上面的https://accounts.google.com/ServiceLogin ?... )开始时,它会中断,因为登录页面的 X-Frame-Options 标头是设置为拒绝。

我确实尝试了 HTMLoutput.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) (请参阅https://developers.google.com/apps-script/reference/html/html-output#setxframeoptionsmodemode),但我很确定导致的问题加载不正确的 Google 协作平台 iFrame 不是我的应用,而是 Google 的登录页面。

链接到 Google 网站: https ://sites.google.com/view/create-user-filter-views/home

链接到应用程序脚本 Web 应用程序: https ://script.google.com/macros/s/AKfycbzizTNkflSXZbKSF8TTxTR5QoF4LAhPPuSq-1juFdIOdL_IlFM/exec

Google 提供的关于如何在新站点中嵌入 Apps 脚本的文档: https ://developers.google.com/apps-script/guides/web#embedding_a_web_app_in_new_sites

如何从 Google 协作平台授权新用户使用我的网络应用程序?

我是否需要先将它们引导到我已发布的应用程序脚本站点以通过授权流程,然后引导它们返回到我的 Google 站点(这显然是一个糟糕的选择)?

4

1 回答 1

3

首先,你的分析是对的。Google 的登录页面(实际上是 Google 托管内容的很大一部分)将 X-Frame-Options 设置为拒绝,并且由于该设置,重定向被阻止在 iframe 内加载。如果用户已经登录到 Google,但尚未授权该应用程序,我相信大多数时候他们应该在 iframe 中看到授权对话流程而没有错误(Alan Wells 报告的内容)。但是,我没有完全测试,它可能是针对同时登录多个用户(例如登录多个 Gmail)的用户,它会将您踢出登录页面并触发 X-Frame-Options 块。

不管怎样,经过一番挖掘,我找到了一个可行的解决方案。这有点笨拙,因为 Apps Script 对可以使用的内容设置了各种限制。例如,我首先想使用postMessage将消息从嵌入的 iframe 传递到父页面,如果父页面在 X # 秒内没有收到消息,它将假定 iframe 加载失败并重定向用户登录/授权应用程序。唉,postMessageApps Script 不能很好地发挥作用,因为它们是双嵌入 iframe 的。

解决方案:

JSONP:

我得到的第一个解决方案是使用 JSONP 方法。谷歌在这里简要提到了这一点。首先,在 iframe 上放置一个覆盖层,提示用户对应用程序进行身份验证,并提供一个链接。然后您加载应用程序脚本两次,一次作为 iframe,然后再次作为<script></script>标签。如果<script>标签加载成功,它会调用一个回调函数来隐藏提示覆盖,这样下面的 iframe 就会变得可见。

这是我的代码,经过精简,您可以看到它是如何工作的:

嵌入的 HTML:

<style>
.appsWidgetWrapper {
    position: fixed;
}
.appsWidget {
    width: 100%;
    height: 100%;
    min-width: 300px;
    min-height: 300px;
    border: none !important;
}
.loggedOut {
    top: 0px;
    left: 0px;
    position: absolute;
    width: 100%;
    height: 100%;
    background-color: darksalmon;
    text-align: center;
}
</style>

<!-- Script loaded as iframe widget with fallback -->
<div class="appsWidgetWrapper">
    <iframe class="appsWidget" src="https://script.google.com/macros/s/SCRIPT_ID/exec?embedIframe"></iframe>
    <div class="loggedOut">
        <div class="loggedOutContent">
            <div class="loggedOutText">You need to "authorize" this widget.</div>
            <button class="authButton">Log In / Authorize</button>
        </div>
    </div>
</div>

<!-- Define JSONP callback and authbutton redirect-->
<script>
    function authSuccess(email){
        console.log(email);
        // Hide auth prompt overlay
        document.querySelector('.loggedOut').style.display = 'none';
    }
    document.querySelectorAll('.authButton').forEach(function(elem){
        elem.addEventListener('click',function(evt){
            var currentUrl = document.location.href;
            var authPage = 'https://script.google.com/macros/s/SCRIPT_ID/exec?auth=true&redirect=' + encodeURIComponent(currentUrl);
            window.open(authPage,'_blank');
        });
    });
</script>

<!-- Fetch script as JSONP with callback -->
<script src="https://script.google.com/macros/s/SCRIPT_ID/exec?jsonpCallback=authSuccess"></script>

和 Code.gs(应用程序脚本)

function doGet(e) {
    var email = Session.getActiveUser().getEmail();

    if (e.queryString && 'jsonpCallback' in e.parameter){
        // JSONP callback
        // Get the string name of the callback function
        var cbFnName = e.parameter['jsonpCallback'];
        // Prepare stringified JS that will get evaluated when called from <script></script> tag
        var scriptText = "window." + cbFnName + "('" + email + "');";
        // Return proper MIME type for JS
        return ContentService.createTextOutput(scriptText).setMimeType(ContentService.MimeType.JAVASCRIPT);
    }

    else if (e.queryString && ('auth' in e.parameter || 'redirect' in e.parameter)){
        // Script was opened in order to auth in new tab
        var rawHtml = '<p>You have successfully authorized the widget. You can now close this tab and refresh the page you were previously on.</p>';
        if ('redirect' in e.parameter){
            rawHtml += '<br/><a href="' + e.parameter['redirect'] + '">Previous Page</a>';
        }
        return HtmlService.createHtmlOutput(rawHtml);
    }
    else {
        // Display HTML in iframe
        var rawHtml = "<h1>App Script successfully loaded in iframe!</h1>"
            + "\n"
            + "<h2>User's email used to authorize: <?= authedEmail ?></h2>";
        var template = HtmlService.createTemplate(rawHtml);
        template.authedEmail = email;
        return template.evaluate().setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
    }
}

在这个例子中,“authSuccess”是我的 JSONP 回调函数,如果脚本成功,它应该被授权用户的电子邮件调用。否则,如果用户需要登录或授权,则不会,并且叠加层将保持可见并阻止 iframe 错误显示给用户。

contentWindow.length

感谢 TheMaster 在这篇文章上留下的评论,以及他的链接答案,我了解到另一种在这种情况下有效的方法。即使在跨域场景中,某些属性也会从 iframe 中公开,其中之一是{iframeElem}.contentWindow.length. 这是 的代理值window.length,它iframe窗口内的元素数。1由于 Google Apps 脚本始终将返回的 HTML 包装在 iframe 中(给我们提供了双重嵌套 iframe),如果 iframe 加载,则该值将是 ,或者如果加载0失败,则为 。我们可以使用这些因素组合来制作另一种不需要 JSONP 的方法。

嵌入式 HTML:

<style>
.appsWidgetWrapper {
    position: fixed;
}
.appsWidget {
    width: 100%;
    height: 100%;
    min-width: 300px;
    min-height: 300px;
    border: none !important;
}
.loggedOut {
    top: 0px;
    left: 0px;
    position: absolute;
    width: 100%;
    height: 100%;
    background-color: darksalmon;
    text-align: center;
}
</style>

<!-- Script loaded as iframe widget with fallback -->
<div class="appsWidgetWrapper">
    <iframe class="appsWidget" src="https://script.google.com/macros/s/SCRIPT_ID/exec"></iframe>
    <div class="loggedOut">
        <div class="loggedOutContent">
            <div class="loggedOutText">You need to "authorize" this widget.</div>
            <button class="authButton">Log In / Authorize</button>
        </div>
    </div>
</div>

<!-- Check iframe contentWindow.length -->
<script>
    // Give iframe some time to load, while re-checking
    var retries = 5;
    var attempts = 0;
    var done = false;
    function checkIfAuthed() {
        attempts++;
        console.log(`Checking if authed...`);
        var iframe = document.querySelector('.appsWidget');
        if (iframe.contentWindow.length) {
            // User has signed in, preventing x-frame deny issue
            // Hide auth prompt overlay
            document.querySelector('.loggedOut').style.display = 'none';
            done = true;
        } else {
            console.log(`iframe.contentWindow.length is falsy, user needs to auth`);
        }

        if (done || attempts >= retries) {
            clearInterval(authChecker);
        }
    }
    window.authChecker = setInterval(checkIfAuthed, 200);
    document.querySelectorAll('.authButton').forEach(function(elem){
        elem.addEventListener('click',function(evt){
            var currentUrl = document.location.href;
            var authPage = 'https://script.google.com/macros/s/SCRIPT_ID/exec?auth=true&redirect=' + encodeURIComponent(currentUrl);
            window.open(authPage,'_blank');
        });
    });
</script>

代码.js:

function doGet(e) {
    var email = Session.getActiveUser().getEmail();

    if (e.queryString && ('auth' in e.parameter || 'redirect' in e.parameter)){
        // Script was opened in order to auth in new tab
        var rawHtml = '<p>You have successfully authorized the widget. You can now close this tab and refresh the page you were previously on.</p>';
        if ('redirect' in e.parameter){
            rawHtml += '<br/><a href="' + e.parameter['redirect'] + '">Previous Page</a>';
        }
        return HtmlService.createHtmlOutput(rawHtml);
    }
    else {
        // Display HTML in iframe
        var rawHtml = "<h1>App Script successfully loaded in iframe!</h1>"
            + "\n"
            + "<h2>User's email used to authorize: <?= authedEmail ?></h2>";
        var template = HtmlService.createTemplate(rawHtml);
        template.authedEmail = email;
        return template.evaluate().setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL);
    }
}

完整演示链接:

我还在Github 上发布了完整的代码,其中的结构可能更容易看到。

于 2019-06-16T00:57:25.593 回答