如何将需要授权的独立 Google Apps 脚本网络应用程序嵌入到新的 Google 站点中?
How do I embed a standalone Google Apps Script web app that requires authorization into the new Google Sites?
我创建了一个独立的 Google Apps Script 网络应用程序,我正试图将其嵌入到新的 Google 站点中。当我登录到用于创建 Apps 脚本项目的帐户时,它可以正常工作。但是,如果我登录到另一个尚未授权 Web 应用程序的帐户,Google 站点页面会加载,但带有嵌入式 Apps 脚本项目的 iFrame 不会正确加载。
iFrame 显示 "accounts.google.com refused to connect" 而控制台显示 "Refused to display '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 header页面设置为拒绝。
我确实用 HTMLoutput.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) 进行了实验(参见 https://developers.google.com/apps-script/reference/html/html-output#setxframeoptionsmodemode),但我很确定导致 Google Sites iFrame 加载不正确的问题不是我的应用程序,而是 Google 页面上的签名。
Link 至 Google 站点:
https://sites.google.com/view/create-user-filter-views/home
Link 到 Apps 脚本网络应用程序:
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 个站点访问我的 Web 应用程序?
我是否需要先将他们引导至我发布的应用程序脚本站点以完成授权流程,然后将他们引导至 return 至我的 Google 站点(这显然是一个糟糕的选择)?
首先,你的分析是对的。 Google 的登录页面(实际上 Google 托管内容的很大一部分)已将 X-Frame-Options 设置为拒绝,由于该设置,重定向被阻止加载到 iframe 中。如果用户已经登录到 Google,但尚未授权该应用程序,我相信大多数时候他们应该会在 iframe 中看到授权对话框流而不会出现错误(Alan Wells 报告的内容)。但是,我没有完全测试,它可能是针对同时登录多个用户(例如登录多个 Gmail),它会将您踢出登录页面并触发 X-Frame-Options 块。
无论如何,经过一番挖掘,我找到了一个可行的解决方案。这有点笨拙,因为 Apps 脚本对可以使用的内容施加了各种限制。例如,我首先想使用 postMessage
将消息从嵌入式 iframe 传递到父页面,如果父页面在 X # 秒内没有收到消息,它会认为 iframe 加载失败并将重定向用户登录/授权应用程序。唉,postMessage
不能很好地与 Apps 脚本配合使用,因为它们 double-embed iframe。
解决方案:
JSONP:
我得到的第一个解决方案是使用 JSONP 方法。 Google here 简要提到了这一点。首先,在 iframe 上放置一个叠加层,提示用户对应用程序进行身份验证,并使用 link 进行验证。然后加载应用脚本两次,一次作为 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(Apps 脚本)
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 在此 post 和 , I learned of another approach that works in this instance. Certain properties are exposed from the iframe, even in a cross-origin scenario, and one of them is {iframeElem}.contentWindow.length
. This is a proxied value of window.length
, which is the number of iframe
elements within the window 上留下的评论。由于 Google Apps 脚本总是将返回的 HTML 包装在一个 iframe 中(为我们提供双重嵌套的 iframe),如果加载 iframe,该值将是 1
,或者 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>
Code.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);
}
}
完整演示 Link:
我还 post 编辑了完整代码 on Github,其中的结构可能更容易看清。
我创建了一个独立的 Google Apps Script 网络应用程序,我正试图将其嵌入到新的 Google 站点中。当我登录到用于创建 Apps 脚本项目的帐户时,它可以正常工作。但是,如果我登录到另一个尚未授权 Web 应用程序的帐户,Google 站点页面会加载,但带有嵌入式 Apps 脚本项目的 iFrame 不会正确加载。
iFrame 显示 "accounts.google.com refused to connect" 而控制台显示 "Refused to display '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 header页面设置为拒绝。
我确实用 HTMLoutput.setXFrameOptionsMode(HtmlService.XFrameOptionsMode.ALLOWALL) 进行了实验(参见 https://developers.google.com/apps-script/reference/html/html-output#setxframeoptionsmodemode),但我很确定导致 Google Sites iFrame 加载不正确的问题不是我的应用程序,而是 Google 页面上的签名。
Link 至 Google 站点: https://sites.google.com/view/create-user-filter-views/home
Link 到 Apps 脚本网络应用程序: 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 个站点访问我的 Web 应用程序?
我是否需要先将他们引导至我发布的应用程序脚本站点以完成授权流程,然后将他们引导至 return 至我的 Google 站点(这显然是一个糟糕的选择)?
首先,你的分析是对的。 Google 的登录页面(实际上 Google 托管内容的很大一部分)已将 X-Frame-Options 设置为拒绝,由于该设置,重定向被阻止加载到 iframe 中。如果用户已经登录到 Google,但尚未授权该应用程序,我相信大多数时候他们应该会在 iframe 中看到授权对话框流而不会出现错误(Alan Wells 报告的内容)。但是,我没有完全测试,它可能是针对同时登录多个用户(例如登录多个 Gmail),它会将您踢出登录页面并触发 X-Frame-Options 块。
无论如何,经过一番挖掘,我找到了一个可行的解决方案。这有点笨拙,因为 Apps 脚本对可以使用的内容施加了各种限制。例如,我首先想使用 postMessage
将消息从嵌入式 iframe 传递到父页面,如果父页面在 X # 秒内没有收到消息,它会认为 iframe 加载失败并将重定向用户登录/授权应用程序。唉,postMessage
不能很好地与 Apps 脚本配合使用,因为它们 double-embed iframe。
解决方案:
JSONP:
我得到的第一个解决方案是使用 JSONP 方法。 Google here 简要提到了这一点。首先,在 iframe 上放置一个叠加层,提示用户对应用程序进行身份验证,并使用 link 进行验证。然后加载应用脚本两次,一次作为 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(Apps 脚本)
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 在此 post 和 {iframeElem}.contentWindow.length
. This is a proxied value of window.length
, which is the number of iframe
elements within the window 上留下的评论。由于 Google Apps 脚本总是将返回的 HTML 包装在一个 iframe 中(为我们提供双重嵌套的 iframe),如果加载 iframe,该值将是 1
,或者 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>
Code.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);
}
}
完整演示 Link:
我还 post 编辑了完整代码 on Github,其中的结构可能更容易看清。