谷歌OAuth2与应用脚本在IFRAME沙箱中

5

就 Web 开发而言,我是个新手,对于 Google App Scripts 和 OAuth2.0 更是如此。话虽如此,我进行了足够的研究并尝试了几种技巧,但仍无法解决这个问题。

我从这里借鉴了样本:

Google Developers - Client API Library

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

index.html 页面由 HTML 服务提供,SandBox Mode 被设置为 IFRAME。如果我在浏览器窗口中加载 URL(比如使用隐身模式),并点击 “授权” 按钮,它会打开 Google 登录窗口。但是,在登录后,它会弹出两个新标签页,显示如下信息:

Please close this window

而原始浏览器窗口则没有任何变化。

JavaScript 控制台显示以下错误信息:

Unsafe JavaScript attempt to initiate navigation for frame with URL '' from frame with URL https://accounts.google.com/o/oauth2/postmessageRelay?parent=https%3A%2F%2F…6lxdpyio6iqy-script.googleusercontent.com#rpctoken=288384029&forcesecure=1. The frame attempting navigation is sandboxed, and is therefore disallowed from navigating its ancestors.

从这些信息来看,似乎是使用 IFRAME 的影响,并且某种安全功能正在防止回调被传递到原始窗口。如果我重新加载原始窗口,事情就可以正常运行。但这不是我理想的解决方案。

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

谢谢, Pavan

编辑:以下是我正在尝试的示例代码。为使其正常工作,您需要拥有客户端 ID 和 API 密钥,并在 Google 控制台中设置 JS 来源:

Code.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>

如果您想使用Google账户强制登录,则可以将发布权限设置为:“谁可以访问该应用程序:任何人”,而不是“任何人,包括匿名用户”。这样,您甚至不需要oAuth2。 - Alan Wells
你看过这份文档吗?Apps Script - 使用OAuth进行请求 - Alan Wells
“谁可以访问该应用程序:任何人” 的问题在于,当该应用程序作为 Google 网站上的小工具使用时(这是我最终想要做的),如果用户尚未登录,则不会显示登录屏幕,而只会显示一个空白框架。如果它被用作 Web 应用程序,则会显示登录屏幕,但不包括小工具。我已经看过那份文档,并阅读了许多其他文档/教程/示例,但仍然不确定如何解决这个问题。 - Pavan Deolasee
你可以将Apps Script绑定到网站而无需将其制作成小工具。您必须前往“管理网站”、“Apps Script”。我在想那是否可行? - Alan Wells
如果我没记错的话,我也尝试过那个,但没有太大的成功。可以再试一次,但我不太有希望。 - Pavan Deolasee
如果您发布所有的代码,有人可能会注意到一些可以帮助的东西。 - Alan Wells
1个回答

1
发现了一个解决方案...不太好但是有效 oO:
关键是在授权窗口关闭之前删除oauth2relay iframes。在窗口关闭后,你必须再次添加框架并进行立即请求,如果成功则表示用户已经授权该应用程序。
注意: 此脚本不检查用户是否已经注销或令牌是否已过期,只要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>

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