如何授权GAS Web App代表用户执行操作?解决Telegram集成权限问题
解决方案:GAS Web App + Telegram 机器人访问用户表格权限问题
核心问题分析
Web App以脚本所有者身份运行时,无法直接访问用户的私有Google表格,且Telegram机器人的匿名访问要求Web App不能切换为"访问者身份"执行,同时不能要求用户共享表格。以下是适配该场景的三种可行方案:
方案一:用户OAuth授权 + 存储Token访问表格
让用户通过OAuth授权Web App访问其表格权限,将用户的访问令牌与Telegram ID关联存储,Web App使用用户的令牌调用Sheets API完成操作。
步骤:
创建OAuth客户端ID
- 打开Google Cloud Console(对应脚本关联的云项目),进入"API和服务" > "凭据"
- 创建"OAuth 2.0客户端ID",类型选"Web应用",添加Web App的URL作为授权重定向URI(例如
https://script.google.com/macros/s/[WebAppID]/exec)
生成用户授权链接
构造类似clasp的授权链接,包含必要权限和参数:https://accounts.google.com/o/oauth2/v2/auth?access_type=offline&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fspreadsheets&response_type=code&client_id=你的客户端ID&redirect_uri=你的WebApp回调URI&state=用户TelegramIDscope:用https://www.googleapis.com/auth/spreadsheets(全权限)或https://www.googleapis.com/auth/drive.file(仅授权用户指定的表格)state参数用于传递用户的Telegram ID,回调时关联令牌
处理授权回调
在Web App中添加回调函数,接收授权码并交换令牌:function doGet(e) { if (e.parameter.code) { // 交换授权码为access_token和refresh_token const tokenResponse = UrlFetchApp.fetch('https://oauth2.googleapis.com/token', { method: 'post', payload: { code: e.parameter.code, client_id: '你的客户端ID', client_secret: '你的客户端密钥', redirect_uri: '你的WebApp回调URI', grant_type: 'authorization_code' } }); const tokenData = JSON.parse(tokenResponse.getContentText()); // 将令牌与Telegram ID关联存储到脚本属性 const scriptProps = PropertiesService.getScriptProperties(); scriptProps.setProperty(`token_${e.parameter.state}`, JSON.stringify({ access_token: tokenData.access_token, refresh_token: tokenData.refresh_token, expires_at: Date.now() + tokenData.expires_in * 1000 })); return HtmlService.createHtmlOutput('授权成功,可关闭页面'); } // 显示授权页面链接 return HtmlService.createHtmlOutput(`<a href="你的授权链接">点击授权访问你的表格</a>`); }使用用户令牌访问表格
不再直接调用SpreadsheetApp.openById,改用Sheets API:function writeToUserSheet(telegramId, sheetId, data) { const scriptProps = PropertiesService.getScriptProperties(); const tokenInfo = JSON.parse(scriptProps.getProperty(`token_${telegramId}`)); // 检查令牌是否过期,过期则刷新 if (Date.now() > tokenInfo.expires_at) { const refreshResponse = UrlFetchApp.fetch('https://oauth2.googleapis.com/token', { method: 'post', payload: { refresh_token: tokenInfo.refresh_token, client_id: '你的客户端ID', client_secret: '你的客户端密钥', grant_type: 'refresh_token' } }); const newTokenData = JSON.parse(refreshResponse.getContentText()); tokenInfo.access_token = newTokenData.access_token; tokenInfo.expires_at = Date.now() + newTokenData.expires_in * 1000; scriptProps.setProperty(`token_${telegramId}`, JSON.stringify(tokenInfo)); } // 调用Sheets API写入数据 UrlFetchApp.fetch(`https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/Sheet1!A1:append?valueInputOption=USER_ENTERED`, { method: 'post', headers: { Authorization: `Bearer ${tokenInfo.access_token}` }, contentType: 'application/json', payload: JSON.stringify({ values: [data] }) }); }
方案二:时间驱动触发器(无需OAuth)
利用GAS的时间驱动触发器,让表格写入操作以用户身份执行,Web App仅负责接收Telegram消息并创建触发器。
步骤:
插件安装时关联用户与Telegram ID
在插件的onInstall或onOpen函数中,让用户绑定Telegram ID:function onOpen() { SpreadsheetApp.getUi().createMenu('你的插件') .addItem('绑定Telegram ID', 'showTelegramBindDialog') .addToUi(); } function showTelegramBindDialog() { const html = HtmlService.createHtmlOutput(` <input type="text" id="telegramId" placeholder="输入你的Telegram ID"> <button onclick="bind()">绑定</button> <script> function bind() { const telegramId = document.getElementById('telegramId').value; google.script.run.withSuccessHandler(() => alert('绑定成功')).bindTelegramId(telegramId); } </script> `); SpreadsheetApp.getUi().showModalDialog(html, '绑定Telegram'); } function bindTelegramId(telegramId) { const userEmail = Session.getActiveUser().getEmail(); const scriptProps = PropertiesService.getScriptProperties(); scriptProps.setProperty(`user_${telegramId}`, userEmail); scriptProps.setProperty(`sheet_${telegramId}`, SpreadsheetApp.getActiveSpreadsheet().getId()); }Web App接收消息并创建触发器
function doPost(e) { const telegramData = JSON.parse(e.postData.contents); const telegramId = telegramData.message.from.id; const scriptProps = PropertiesService.getScriptProperties(); const userEmail = scriptProps.getProperty(`user_${telegramId}`); const sheetId = scriptProps.getProperty(`sheet_${telegramId}`); if (!userEmail || !sheetId) return ContentService.createTextOutput('未绑定账号'); // 创建一次性时间驱动触发器,以用户身份执行写入 const trigger = ScriptApp.newTrigger('writeToSheet') .timeBased() .after(1000) // 1秒后执行 .forUser(userEmail) .create(); // 存储消息数据到脚本属性 scriptProps.setProperty(`msg_${trigger.getUniqueId()}`, JSON.stringify({ sheetId, content: telegramData.message.text })); return ContentService.createTextOutput('消息已接收'); } function writeToSheet() { const trigger = ScriptApp.getTriggerSource(); const scriptProps = PropertiesService.getScriptProperties(); const msgData = JSON.parse(scriptProps.getProperty(`msg_${trigger.getUniqueId()}`)); // 以用户身份访问表格 const sheet = SpreadsheetApp.openById(msgData.sheetId).getSheetByName('Sheet1'); sheet.appendRow([new Date(), msgData.content]); // 删除触发器和临时数据 ScriptApp.deleteTrigger(trigger); scriptProps.deleteProperty(`msg_${trigger.getUniqueId()}`); }注意:触发器执行有一定延迟(通常几秒到几分钟),适合对实时性要求不高的场景。
方案三:服务账号(适合企业域用户)
如果是企业G Suite域用户,可使用服务账号的域范围授权,无需用户手动共享表格。个人用户需将表格共享给服务账号邮箱(隐私问题需权衡)。
步骤:
创建服务账号并下载密钥
在Google Cloud Console创建服务账号,下载JSON格式的密钥文件,将密钥内容存储到脚本属性中。使用服务账号访问表格
function getServiceAccountToken() { const scriptProps = PropertiesService.getScriptProperties(); const serviceAccountKey = JSON.parse(scriptProps.getProperty('service_account_key')); const header = Utilities.base64EncodeWebSafe(JSON.stringify({ alg: 'RS256', typ: 'JWT' })); const payload = Utilities.base64EncodeWebSafe(JSON.stringify({ iss: serviceAccountKey.client_email, scope: 'https://www.googleapis.com/auth/spreadsheets', aud: 'https://oauth2.googleapis.com/token', exp: Math.floor(Date.now() / 1000) + 3600, iat: Math.floor(Date.now() / 1000) })); const signature = Utilities.base64EncodeWebSafe(Utilities.computeRsaSha256Signature(`${header}.${payload}`, serviceAccountKey.private_key)); const jwt = `${header}.${payload}.${signature}`; const tokenResponse = UrlFetchApp.fetch('https://oauth2.googleapis.com/token', { method: 'post', payload: { grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer', assertion: jwt } }); return JSON.parse(tokenResponse.getContentText()).access_token; } function writeToSheetWithServiceAccount(sheetId, data) { const token = getServiceAccountToken(); UrlFetchApp.fetch(`https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/Sheet1!A1:append?valueInputOption=USER_ENTERED`, { method: 'post', headers: { Authorization: `Bearer ${token}` }, contentType: 'application/json', payload: JSON.stringify({ values: [data] }) }); }
内容的提问来源于stack exchange,提问作者Basil Vlasyuk
相关产品推荐
相关产品推荐

