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

Pav*_*see 5 javascript iframe google-apps-script oauth-2.0

就 Web 开发而言,我是新手,对于 Google App Scripts 和 OAuth2.0 更是如此。话虽如此,我已经研究了足够多,也尝试了一些技巧,但仍然无法解决这个问题。

我从这里借了样品:

Google Developers - 客户端 API 库

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

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

请关闭此窗口

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

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

不安全的 JavaScript 尝试从带有 URL 的框架中启动带有 URL '' 的框架的导航 https://accounts.google.com/o/oauth2/postmessageRelay?parent=https%3A%2F%2F…6lxdpyio6iqy-script.googleusercontent.com#rpctoken=288384029&forcesecure=1。尝试导航的框架被沙盒化,因此不允许导航其祖先。

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

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

谢谢,帕万

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

代码.gs

function doGet(e) {
    return HtmlService.createHtmlOutputFromFile('index').setSandboxMode(HtmlService.SandboxMode.IFRAME);
}
Run Code Online (Sandbox Code Playgroud)

索引.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>
Run Code Online (Sandbox Code Playgroud)

小智 1

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

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

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

代码.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;
}
Run Code Online (Sandbox Code Playgroud)

索引.html:

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

    </div>
  </body>
</html>
Run Code Online (Sandbox Code Playgroud)

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>
Run Code Online (Sandbox Code Playgroud)