You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何授权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完成操作。

步骤:

  1. 创建OAuth客户端ID

    • 打开Google Cloud Console(对应脚本关联的云项目),进入"API和服务" > "凭据"
    • 创建"OAuth 2.0客户端ID",类型选"Web应用",添加Web App的URL作为授权重定向URI(例如https://script.google.com/macros/s/[WebAppID]/exec)
  2. 生成用户授权链接
    构造类似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=用户TelegramID
    
    • scope:用https://www.googleapis.com/auth/spreadsheets(全权限)或https://www.googleapis.com/auth/drive.file(仅授权用户指定的表格)
    • state参数用于传递用户的Telegram ID,回调时关联令牌
  3. 处理授权回调
    在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>`);
    }
    
  4. 使用用户令牌访问表格
    不再直接调用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消息并创建触发器。

步骤:

  1. 插件安装时关联用户与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());
    }
    
  2. 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域用户,可使用服务账号的域范围授权,无需用户手动共享表格。个人用户需将表格共享给服务账号邮箱(隐私问题需权衡)。

步骤:

  1. 创建服务账号并下载密钥
    在Google Cloud Console创建服务账号,下载JSON格式的密钥文件,将密钥内容存储到脚本属性中。

  2. 使用服务账号访问表格

    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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 00:02:03