如何正确配置 GAS Web 应用程序(作为另一个用户)以使用 OAuth2 执行 GAS API 可执行文件(作为我)?
How to Properly Configure GAS Web App (as another user) to Execute GAS API Executable (as me) using OAuth2?
问题
经过几天的阅读和尝试试错,我正在尝试从 GAS Web 应用程序(作为任何 Google 用户执行)调用 GAS API 可执行文件(以我的身份执行),但在 Reviewing/Granting 权限后始终收到错误消息:
*"Error: Access not granted or expired."*
这不是来自服务器的响应,而只是来自 OAuth2 库的通知:"如果用户的访问不是 g运行ted 或已经过期了。"
因此说明和问答中似乎缺少一些其他明显的步骤。不知何故,在完成所有设置后,网络应用程序无法访问 API 可执行文件。
我也花了几个小时写了这个问题,制定了一个最小的test/example。 Here are the files in Google Drive, for viewing directly.
期望的结果
期望的结果是能够让其他用户像他们自己一样使用 Web 应用程序,并从该应用程序以我的身份执行 API 可执行文件。
问题
我的配置 and/or 代码有什么问题,我如何从 API 可执行文件接收数据?
我试过的
我结合了各种教程和问答,并试图做一个最小的例子。最密切相关的三个是:
...Service accounts, while applicable, are not the best fit for this use-case. They are better suited to situations where the service account acts as a proxy for multiple users...
...Basically, you'll need to generate OAuth2 credentials specific to
your account and use the OAuth2 library to generate access tokens.
That access token can then be used to make direct calls against the
Spreadsheet REST API OR alternatively, the Apps Script API (formerly
the Execution API) to invoke a function in the script under your own
authority...
第一个link似乎直接适用于我的场景。然而,说明很少,尽管我已尽力遵循它们。第二个也适用,但稀少。第三个是相关的,但实际上与我想做的相反,所以帮助有限。
GCP 中的步骤总结
这是我在 console.cloud.google.com 中所做的:
- 创建了一个名为“apiExecTest”的新项目。
- 在“APIs & Services”中,启用了两个 APIs:
- Apps 脚本 API(必要时不确定)
- Google Sheets API(必要时不确定)
- 在“APIs & Services”中,配置了 Oauth 同意屏幕
- 内部
- 设置应用程序名称、用户支持邮箱和开发者联系邮箱。什么也没做。没有设置“应用域”和“授权域”。
- 为 Apps 脚本和 Sheet 添加了所有 61 个范围(必要时不确定)
- 在“APIs & Services”中,创建了凭据
- OAuth 客户端 ID
- Web 应用程序
- 已添加客户名称。
- 添加授权重定向 URI:
https://script.google.com/macros/d/1zj4ovqMWoCUgBxJJ8u518TOEKlckeIazVBL4ASdYFiVmjoZz9BLXbJ7y/usercallback
- 已获取客户端 ID 和客户端密码以插入到 webApp 代码中。
GAS 中的步骤总结
这是我在 Google Drive / Apps 脚本中所做的。可以查看文件here:
在 Google 驱动器中创建了一个包含三样东西的新文件夹:
- GAS 文件:“webApp”
- 部署为网络应用程序
- 执行为:访问网络应用程序的用户
- 谁有访问权限:拥有 Google 帐户的任何人
- GAS 文件:“apiExec”
- 部署为API可执行文件
- 谁有访问权限:拥有 Google 帐户的任何人
- Google Sheet: sSheet
- 不与任何人共享,归我所有。
向apiExec添加了一个基本函数,获取s[=279=中第一个sheet的第一个单元格],并通过在 GAS 编辑器中执行它并观察控制台输出来确认它有效。
将 OAuth2 库添加到 webApp 作为 oauth2.gs, copy/pasted 来自 GitHub. Setup and configured setClientId(), setClientSecret(), API URL and other settings per the readme和上面引用的例子。对于 setScope(),我使用了:
.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/spreadsheets')
向 webApp 添加了一个基本功能,可以调用 apiExec 从 [=132= 获取数据]sSheet.
将以下内容添加到 webApp appsscript.json(不确定是否正确,已尝试变体) :
"oauthScopes": ["https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/spreadsheets"]
我将 apiExec 和 webApp 的 GCP 项目编号更改为在以上步骤。
然后我在 GAS 编辑器中执行了 webApp 的 doGet()
函数。它确实要求授权,我 g运行ted。授权后,继续执行,抛出上述错误。我还 运行 函数 via webApp's URL,这当然也会导致错误。
在多次尝试之后,花了几天时间阅读和反复试验,但我没有取得任何进展。非常感谢任何帮助。
为了完整起见,这里是 GAS 文件的内容:
apiExec
appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"executionApi": {
"access": "ANYONE"
}
}
Code.gs
function doPost() {
var spreadsheet = SpreadsheetApp.openById("1aIMv1iH6rxDwXLx-i0uYi3D783dCtlMZo6pXJGztKTY");
var sheet = spreadsheet.getSheetByName("test sheet");
var data = sheet.getRange("A1").getValues()
console.log(data)
return data;
}
网络应用程序
appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/spreadsheets"
],
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "ANYONE"
}
}
Code.gs
function doGet(e) {
var myParam = "someParam";
console.log(myParam);
var apiExecResponse = makeRequest('doPost', [myParam]);
console.log(apiExecResponse);
var appsScriptService = getService();
if (!appsScriptService.hasAccess()) {
// This block should only run once, when I authenticate as myself to create the refresh token.
var authorizationUrl = appsScriptService.getAuthorizationUrl();
var htmlOutput = HtmlService.createHtmlOutput('<a href="' + authorizationUrl + '" target="_blank">Authorize</a>.');
htmlOutput.setTitle('GAS Authentication');
return htmlOutput;
}
else {
console.log("It worked: " + myParam + " " + apiExecResponse);
htmlOutput.setTitle("The Results");
return HtmlService.createHtmlOutput("<p>It worked: " + myParam + " " + apiExecResponse + "</p>");
}
}
function getService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('apiExecService')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('390208108732-s7geeikfvnqd52a0fhf6e015ucam0vqk.apps.googleusercontent.com')
.setClientSecret('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')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes to request (space-separated for Google services).
.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/spreadsheets')
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
//.setParam('login_hint', Session.getEffectiveUser().getEmail())
// Requests offline access.
.setParam('access_type', 'offline')
// Consent prompt is required to ensure a refresh token is always
// returned when requesting offline access.
.setParam('prompt', 'consent');
}
function authCallback(request) {
var apiExecService = getService();
var isAuthorized = apiExecService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
}
else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
function makeRequest(functionName, paramsArray) {
console.log("Running " + functionName + " via 'makeRequest'.");
var apiExecUrl = 'https://script.googleapis.com/v1/scripts/AKfycbzHV5_Jl2gJVv0wDVp93wE0BYfxNrOXXKjIAmOoRu3D8W6CeqSQM9JKe8pOYUK4fqM_:run';
var payload = JSON.stringify({
"function": functionName,
"parameters": paramsArray,
"devMode": false
});
var params = {
method:"POST",
headers: {
Authorization: 'Bearer ' + getService().getAccessToken()
},
payload: payload,
contentType: "application/json",
muteHttpExceptions: true
};
var result = UrlFetchApp.fetch(apiExecUrl, params);
return result;
}
OAuth2.gs
See: https://github.com/googleworkspace/apps-script-oauth2/blob/master/dist/OAuth2.gs
如果我没理解错的话,你现在的流程是这样的:
- 使用 OAuth2 库为您自己的 Google 帐户执行 one-time 身份验证令牌捕获。
- 使用该存储的令牌对 API 可执行文件的请求进行身份验证(当 运行 将 Web 应用程序作为另一个用户时)。
Apps 脚本有一个 built-in 方法来完成第 1 步:ScriptApp.getOAuthToken()
,所以我不确定您是否需要 OAuth2 库。 (您需要该库来授权 Google 以外的服务。)
也许您可以通过执行以下操作完全避免使用 OAuth2 库:
将此功能添加到您的 Web 应用程序项目中,并 运行 从编辑器中添加一次,即在您自己的授权下:
function storeOauthToken() {
PropertiesService.getScriptProperties().setProperty(
'myToken',
ScriptApp.getOAuthToken()
)
}
从这个
更改您的 webApp 项目的 makeRequest
函数中的 headers
headers: {
Authorization: 'Bearer ' + getService().getAccessToken()
},
对此:
headers: {
Authorization: 'Bearer ' + PropertiesService.getScriptProperties().getProperty('myToken')
},
我创建了您的项目的副本,并且能够确认该技术有效。
令牌刷新
我认为该令牌可能会像任何其他 OAuth2 令牌一样过期,因此您可能需要定期将定时触发器(同样,在您自己的授权下)设置为 运行 storeOAuthToken()
。
问题
经过几天的阅读和尝试试错,我正在尝试从 GAS Web 应用程序(作为任何 Google 用户执行)调用 GAS API 可执行文件(以我的身份执行),但在 Reviewing/Granting 权限后始终收到错误消息:
*"Error: Access not granted or expired."*
这不是来自服务器的响应,而只是来自 OAuth2 库的通知:"如果用户的访问不是 g运行ted 或已经过期了。"
因此说明和问答中似乎缺少一些其他明显的步骤。不知何故,在完成所有设置后,网络应用程序无法访问 API 可执行文件。
我也花了几个小时写了这个问题,制定了一个最小的test/example。 Here are the files in Google Drive, for viewing directly.
期望的结果
期望的结果是能够让其他用户像他们自己一样使用 Web 应用程序,并从该应用程序以我的身份执行 API 可执行文件。
问题
我的配置 and/or 代码有什么问题,我如何从 API 可执行文件接收数据?
我试过的
我结合了各种教程和问答,并试图做一个最小的例子。最密切相关的三个是:
...Service accounts, while applicable, are not the best fit for this use-case. They are better suited to situations where the service account acts as a proxy for multiple users...
...Basically, you'll need to generate OAuth2 credentials specific to your account and use the OAuth2 library to generate access tokens. That access token can then be used to make direct calls against the Spreadsheet REST API OR alternatively, the Apps Script API (formerly the Execution API) to invoke a function in the script under your own authority...
第一个link似乎直接适用于我的场景。然而,说明很少,尽管我已尽力遵循它们。第二个也适用,但稀少。第三个是相关的,但实际上与我想做的相反,所以帮助有限。
GCP 中的步骤总结
这是我在 console.cloud.google.com 中所做的:
- 创建了一个名为“apiExecTest”的新项目。
- 在“APIs & Services”中,启用了两个 APIs:
- Apps 脚本 API(必要时不确定)
- Google Sheets API(必要时不确定)
- 在“APIs & Services”中,配置了 Oauth 同意屏幕
- 内部
- 设置应用程序名称、用户支持邮箱和开发者联系邮箱。什么也没做。没有设置“应用域”和“授权域”。
- 为 Apps 脚本和 Sheet 添加了所有 61 个范围(必要时不确定)
- 在“APIs & Services”中,创建了凭据
- OAuth 客户端 ID
- Web 应用程序
- 已添加客户名称。
- 添加授权重定向 URI:
https://script.google.com/macros/d/1zj4ovqMWoCUgBxJJ8u518TOEKlckeIazVBL4ASdYFiVmjoZz9BLXbJ7y/usercallback
- 已获取客户端 ID 和客户端密码以插入到 webApp 代码中。
GAS 中的步骤总结
这是我在 Google Drive / Apps 脚本中所做的。可以查看文件here:
在 Google 驱动器中创建了一个包含三样东西的新文件夹:
- GAS 文件:“webApp”
- 部署为网络应用程序
- 执行为:访问网络应用程序的用户
- 谁有访问权限:拥有 Google 帐户的任何人
- GAS 文件:“apiExec”
- 部署为API可执行文件
- 谁有访问权限:拥有 Google 帐户的任何人
- Google Sheet: sSheet
- 不与任何人共享,归我所有。
- GAS 文件:“webApp”
向apiExec添加了一个基本函数,获取s[=279=中第一个sheet的第一个单元格],并通过在 GAS 编辑器中执行它并观察控制台输出来确认它有效。
将 OAuth2 库添加到 webApp 作为 oauth2.gs, copy/pasted 来自 GitHub. Setup and configured setClientId(), setClientSecret(), API URL and other settings per the readme和上面引用的例子。对于 setScope(),我使用了:
.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/spreadsheets')
向 webApp 添加了一个基本功能,可以调用 apiExec 从 [=132= 获取数据]sSheet.
将以下内容添加到 webApp appsscript.json(不确定是否正确,已尝试变体) :
"oauthScopes": ["https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/spreadsheets"]
我将 apiExec 和 webApp 的 GCP 项目编号更改为在以上步骤。
然后我在 GAS 编辑器中执行了 webApp 的
doGet()
函数。它确实要求授权,我 g运行ted。授权后,继续执行,抛出上述错误。我还 运行 函数 via webApp's URL,这当然也会导致错误。
在多次尝试之后,花了几天时间阅读和反复试验,但我没有取得任何进展。非常感谢任何帮助。
为了完整起见,这里是 GAS 文件的内容:
apiExec
appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"executionApi": {
"access": "ANYONE"
}
}
Code.gs
function doPost() {
var spreadsheet = SpreadsheetApp.openById("1aIMv1iH6rxDwXLx-i0uYi3D783dCtlMZo6pXJGztKTY");
var sheet = spreadsheet.getSheetByName("test sheet");
var data = sheet.getRange("A1").getValues()
console.log(data)
return data;
}
网络应用程序
appsscript.json
{
"timeZone": "America/New_York",
"dependencies": {},
"exceptionLogging": "STACKDRIVER",
"runtimeVersion": "V8",
"oauthScopes": [
"https://www.googleapis.com/auth/script.external_request",
"https://www.googleapis.com/auth/spreadsheets"
],
"webapp": {
"executeAs": "USER_ACCESSING",
"access": "ANYONE"
}
}
Code.gs
function doGet(e) {
var myParam = "someParam";
console.log(myParam);
var apiExecResponse = makeRequest('doPost', [myParam]);
console.log(apiExecResponse);
var appsScriptService = getService();
if (!appsScriptService.hasAccess()) {
// This block should only run once, when I authenticate as myself to create the refresh token.
var authorizationUrl = appsScriptService.getAuthorizationUrl();
var htmlOutput = HtmlService.createHtmlOutput('<a href="' + authorizationUrl + '" target="_blank">Authorize</a>.');
htmlOutput.setTitle('GAS Authentication');
return htmlOutput;
}
else {
console.log("It worked: " + myParam + " " + apiExecResponse);
htmlOutput.setTitle("The Results");
return HtmlService.createHtmlOutput("<p>It worked: " + myParam + " " + apiExecResponse + "</p>");
}
}
function getService() {
// Create a new service with the given name. The name will be used when
// persisting the authorized token, so ensure it is unique within the
// scope of the property store.
return OAuth2.createService('apiExecService')
// Set the endpoint URLs, which are the same for all Google services.
.setAuthorizationBaseUrl('https://accounts.google.com/o/oauth2/auth')
.setTokenUrl('https://accounts.google.com/o/oauth2/token')
// Set the client ID and secret, from the Google Developers Console.
.setClientId('390208108732-s7geeikfvnqd52a0fhf6e015ucam0vqk.apps.googleusercontent.com')
.setClientSecret('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')
// Set the name of the callback function in the script referenced
// above that should be invoked to complete the OAuth flow.
.setCallbackFunction('authCallback')
// Set the property store where authorized tokens should be persisted.
.setPropertyStore(PropertiesService.getScriptProperties())
// Set the scopes to request (space-separated for Google services).
.setScope('https://www.googleapis.com/auth/script.external_request https://www.googleapis.com/auth/spreadsheets')
// Below are Google-specific OAuth2 parameters.
// Sets the login hint, which will prevent the account chooser screen
// from being shown to users logged in with multiple accounts.
//.setParam('login_hint', Session.getEffectiveUser().getEmail())
// Requests offline access.
.setParam('access_type', 'offline')
// Consent prompt is required to ensure a refresh token is always
// returned when requesting offline access.
.setParam('prompt', 'consent');
}
function authCallback(request) {
var apiExecService = getService();
var isAuthorized = apiExecService.handleCallback(request);
if (isAuthorized) {
return HtmlService.createHtmlOutput('Success! You can close this tab.');
}
else {
return HtmlService.createHtmlOutput('Denied. You can close this tab');
}
}
function makeRequest(functionName, paramsArray) {
console.log("Running " + functionName + " via 'makeRequest'.");
var apiExecUrl = 'https://script.googleapis.com/v1/scripts/AKfycbzHV5_Jl2gJVv0wDVp93wE0BYfxNrOXXKjIAmOoRu3D8W6CeqSQM9JKe8pOYUK4fqM_:run';
var payload = JSON.stringify({
"function": functionName,
"parameters": paramsArray,
"devMode": false
});
var params = {
method:"POST",
headers: {
Authorization: 'Bearer ' + getService().getAccessToken()
},
payload: payload,
contentType: "application/json",
muteHttpExceptions: true
};
var result = UrlFetchApp.fetch(apiExecUrl, params);
return result;
}
OAuth2.gs
See: https://github.com/googleworkspace/apps-script-oauth2/blob/master/dist/OAuth2.gs
如果我没理解错的话,你现在的流程是这样的:
- 使用 OAuth2 库为您自己的 Google 帐户执行 one-time 身份验证令牌捕获。
- 使用该存储的令牌对 API 可执行文件的请求进行身份验证(当 运行 将 Web 应用程序作为另一个用户时)。
Apps 脚本有一个 built-in 方法来完成第 1 步:ScriptApp.getOAuthToken()
,所以我不确定您是否需要 OAuth2 库。 (您需要该库来授权 Google 以外的服务。)
也许您可以通过执行以下操作完全避免使用 OAuth2 库:
将此功能添加到您的 Web 应用程序项目中,并 运行 从编辑器中添加一次,即在您自己的授权下:
function storeOauthToken() {
PropertiesService.getScriptProperties().setProperty(
'myToken',
ScriptApp.getOAuthToken()
)
}
从这个
更改您的 webApp 项目的makeRequest
函数中的 headers
headers: {
Authorization: 'Bearer ' + getService().getAccessToken()
},
对此:
headers: {
Authorization: 'Bearer ' + PropertiesService.getScriptProperties().getProperty('myToken')
},
我创建了您的项目的副本,并且能够确认该技术有效。
令牌刷新
我认为该令牌可能会像任何其他 OAuth2 令牌一样过期,因此您可能需要定期将定时触发器(同样,在您自己的授权下)设置为 运行 storeOAuthToken()
。