IFRAME沙盒中的Google OAuth2和应用程序脚本

Google OAuth2 and app script in IFRAME sandbox

本文关键字:OAuth2 应用程序 脚本 Google IFRAME      更新时间:2023-09-26

就web开发而言,我是一个新手,在Google App Scripts和OAuth2.0方面更是如此。话虽如此,我已经做了足够的研究,也尝试了一些技巧,但仍然无法克服这个问题。

我从这里借了样品:

谷歌开发者-客户API库

然后用index.html文件创建了一个Apps Script项目,其中包含该页面的代码。我还在开发人员控制台上创建了一个项目,创建了客户端ID、API密钥,并启用了所需的API支持。我还对样本进行了必要的更改,以反映新的客户端ID和API密钥。

index.html页面由html服务提供,SandBox Mode设置为IFRAME。如果我在浏览器窗口中加载URL(比如使用隐姓埋名模式)并单击"授权"按钮,就会打开谷歌登录窗口。但在登录后,它会打开两个新的选项卡,其中包含消息

请关闭此窗口

并且原始浏览器窗口没有显示任何变化。

JavaScript控制台显示如下错误消息:

不安全的JavaScript尝试为URL为"的帧启动导航来自具有URL的帧CCD_ 2。尝试导航的帧是沙盒的,因此被禁止导航其祖先。

从消息来看,这似乎是使用IFRAME的一种效果,某种安全功能阻止了回调传递到原始窗口。如果我重新加载原始窗口,一切正常。但这不是我理想中想要的。

我该如何解决这个问题?这是一个非常简单的项目,如果有帮助的话,我可以提供源代码。

谢谢,帕万

编辑:这是我正在尝试的示例代码。你需要有你的客户ID和API密钥,还需要在谷歌控制台中设置JS的来源,以便工作:

代码.gs

function doGet(e) {
    return HtmlService.createHtmlOutputFromFile('index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
}

index.html

<!--
  Copyright (c) 2011 Google Inc.
  Licensed under the Apache License, Version 2.0 (the "License"); you may not
  use this file except in compliance with the License. You may obtain a copy of
  the License at
  http://www.apache.org/licenses/LICENSE-2.0
  Unless required by applicable law or agreed to in writing, software
  distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
  WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
  License for the specific language governing permissions and limitations under
  the License.
  To run this sample, replace YOUR API KEY with your application's API key.
  It can be found at https://code.google.com/apis/console/?api=plus under API Access.
  Activate the Google+ service at https://code.google.com/apis/console/ under Services
-->
<!DOCTYPE html>
<html>
  <head>
    <meta charset='utf-8' />
  </head>
  <body>
    <!--Add a button for the user to click to initiate auth sequence -->
    <button id="authorize-button" style="visibility: hidden">Authorize</button>
    <script type="text/javascript">
      // Enter a client ID for a web application from the Google Developer Console.
      // The provided clientId will only work if the sample is run directly from
      // https://google-api-javascript-client.googlecode.com/hg/samples/authSample.html
      // In your Developer Console project, add a JavaScript origin that corresponds to the domain
      // where you will be running the script.
      var clientId = 'YOUR_CLIENT_ID';

      // Enter the API key from the Google Develoepr Console - to handle any unauthenticated
      // requests in the code.
      // The provided key works for this sample only when run from
      // https://google-api-javascript-client.googlecode.com/hg/samples/authSample.html
      // To use in your own application, replace this API key with your own.
      var apiKey = 'YOUR API KEY';

      // To enter one or more authentication scopes, refer to the documentation for the API.
      var scopes = 'https://www.googleapis.com/auth/plus.me';
      // Use a button to handle authentication the first time.
      function handleClientLoad() {
        gapi.client.setApiKey(apiKey);
        window.setTimeout(checkAuth,1);
      }
      function checkAuth() {
        gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: true, response_type: 'token'}, handleAuthResult);
      }

      function handleAuthResult(authResult) {
        var authorizeButton = document.getElementById('authorize-button');
        if (authResult && !authResult.error) {
          authorizeButton.style.visibility = 'hidden';
          makeApiCall();
        } else {
          authorizeButton.style.visibility = '';
          authorizeButton.onclick = handleAuthClick;
        }
      }
      function handleAuthClick(event) {
        gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: false, response_type: 'token'}, handleAuthResult);
        return false;
      }
      // Load the API and make an API call.  Display the results on the screen.
      function makeApiCall() {
        gapi.client.load('plus', 'v1', function() {
          var request = gapi.client.plus.people.get({
            'userId': 'me'
          });
          request.execute(function(resp) {
            var heading = document.createElement('h4');
            var image = document.createElement('img');
            image.src = resp.image.url;
            heading.appendChild(image);
            heading.appendChild(document.createTextNode(resp.displayName));
            heading.appendChild(document.createTextNode(resp.emails[0].value));
            document.getElementById('content').appendChild(heading);
          });
        });
      }
    </script>
    <script src="https://apis.google.com/js/client.js?onload=handleClientLoad"></script>
    <div id="content"></div>
    <p>Retrieves your profile name using the Google Plus API.</p>
  </body>
</html>

找到了一个解决方案。。。不好,但有效oO:

诀窍是在auth窗口关闭之前删除oauth2relay iframe。窗口关闭后,您必须再次添加框架并立即请求,如果有效,则用户授权该应用程序。

小心
此脚本不会检查用户是否同时注销或令牌是否过期,只要打开webapp窗口,就会使用相同的令牌。

Code.js:

function doGet(e) {
  return HtmlService.createTemplateFromFile('Index').evaluate().setTitle(formSettings.title).setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
function include(file) {
  return HtmlService.createHtmlOutputFromFile(file).getContent();
}
function doPost(meta) {
  if (!meta || !meta.auth) {
    throw new Error('not authorized');
    return;
  }
  var auth = JSON.parse(UrlFetchApp.fetch('https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=' + meta.auth.access_token, { muteHttpExceptions: true }).getContentText());
  if (auth.error || !auth.email) {
    throw new Error('not authorized');
    return;
  }
  if (typeof this[meta.method + '_'] == 'function') {
    return this[meta.method + '_'](auth.email, meta.data);
  }
  throw new Error('unknown method');
}
function test_(email, data) {
  return email;
}

Index.html:

<html>
  <head>
    <?!= include('JavaScript'); ?>
  </head>
  <body>
    <div class="content-wrapper">
    </div>
  </body>
</html>

Javascript.html:

<script type='text/javascript' src="//ajax.googleapis.com/ajax/libs/jquery/2.1.4/jquery.min.js"></script>
<script type='text/javascript' src="//apis.google.com/js/client.js?onload=apiLoaded" async></script>
<script type='text/javascript'>
    var clientId = '*************-********************************.apps.googleusercontent.com';
    var scopes = ['https://www.googleapis.com/auth/plus.me', 'https://www.googleapis.com/auth/userinfo.email'];
    var loaded = false;
    var auth = null;
    function apiLoaded() {  
      loaded = true;
      login();
    }
    window._open = window.open;
    window._windows = [];
    window.open = function(url) {
      var w = window._open.apply(window,arguments);
      window._windows.push(w);
      return w;
    }
    function login(step) {
      step || (step = 0);
      if (!loaded) {
        return;
      }  
      gapi.auth.authorize({client_id: clientId, scope: scopes, immediate: (step <= 0 || step >= 2) }, function(authResult) {
        if (authResult) {
          if (authResult.error) {
            if (authResult.error == 'immediate_failed' && authResult.error_subtype == 'access_denied' && step <= 0) {
              var interval = setInterval(function() {
                var $ifr = $('iframe');//[id^=oauth2relay]');
                if (!window._windows.length) {
                  clearInterval(interval);
                  return;
                }
                if ($ifr.length) {
                  clearInterval(interval);
                  $ifr.detach();
                  var w = window._windows.pop();
                  if (w) {
                    var interval2 = setInterval(function() {
                      if (w.closed) {
                        clearInterval(interval2);
                        $('body').append($ifr);
                        login(2);
                      }
                    });
                  } else {                
                    $('body').append($ifr);
                  }
                }
              },500);
              login(1);
            } else if (authResult.error == 'immediate_failed' && authResult.error_subtype == 'access_denied' && step >= 2) {
              //user canceled auth
            } else {
              //error
            }
          } else {
            auth = authResult;
            doPost('test', { some: 'data' }, 'test');
          }
        } else {
          //error
        }
      });
    }
    function test() {
      console.log(arguments);
    }
    //with this method you can do a post request to webapp server
    function doPost(method, data, callbackName) {
      data || (data = {});
      google.script.run.withSuccessHandler(onSuccess).withFailureHandler(onError).withUserObject({ callback: callbackName }).doPost({ method: method, data: data, auth: auth });
    }
    function onSuccess(data, meta) {
      if (typeof window[meta.callback] == 'function') {
        window[meta.callback](null, data);
      }
    }
    function onError(err, meta) {
      if (typeof window[meta.callback] == 'function') {
        window[meta.callback](err);
      }
    }
</script>