首先,你的分析是对的。Google 的登录页面(实际上是 Google 托管内容的很大一部分)将 X-Frame-Options 设置为拒绝,并且由于该设置,重定向被阻止在 iframe 内加载。如果用户已经登录到 Google,但尚未授权该应用程序,我相信大多数时候他们应该在 iframe 中看到授权对话流程而没有错误(Alan Wells 报告的内容)。但是,我没有完全测试,它可能是针对同时登录多个用户(例如登录多个 Gmail)的用户,它会将您踢出登录页面并触发 X-Frame-Options 块。
不管怎样,经过一番挖掘,我找到了一个可行的解决方案。这有点笨拙,因为 Apps Script 对可以使用的内容设置了各种限制。例如,我首先想使用postMessage
将消息从嵌入的 iframe 传递到父页面,如果父页面在 X # 秒内没有收到消息,它将假定 iframe 加载失败并重定向用户登录/授权应用程序。唉,postMessage
Apps 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 上发布了完整的代码,其中的结构可能更容易看到。