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

服务账号调用Google Apps Script WebApp遇401/500授权错误求助

使用服务账号向Google App Script WebApp发送POST请求时的401/500授权问题

我在通过Google Cloud Console的服务账号向Google App Script WebApp发送POST请求时,遇到了401 Unauthorized错误,后续调整后又出现500错误。具体情况如下:

一、基础配置与代码

WebApp部署配置

  • 执行身份:访问该WebApp的用户
  • 访问权限:拥有谷歌账号的任何人(只有执行身份设为“我自己”时才能选“任何人”,但我不能用这个配置)

测试用doPost函数

function doPost(e) {
  return ContentService.createTextOutput("Hello world");
}

Cloud Function代码

const axios = require('axios');
const { GoogleAuth } = require('google-auth-library');

exports.calendarWebhook = async (req, res) => {
  try {
    const webAppUrl = process.env.WEB_APP_URL; // 环境变量已确认配置正确
    const auth = new GoogleAuth({
      credentials: JSON.parse(process.env.SERVICE_ACCOUNT_KEY), // 已确认配置正确
      scopes: ['https://www.googleapis.com/auth/script.projects'],
    });
    const client = await auth.getClient();
    const accessToken = await client.getAccessToken();

    const requestBody = {
      channelId: req.header('X-Goog-Channel-ID'),
      resourceId: req.header('X-Goog-Resource-ID'),
      resourceState: req.header('X-Goog-Resource-State'),
      userToken: req.header('X-Goog-Channel-Token'),
      secret: process.env.WEB_APP_SECRET,
    };

    const response = await axios.post(webAppUrl, requestBody, {
      headers: {
        Authorization: `Bearer ${accessToken.token}`,
        'Content-Type': 'application/json',
      },
    });

    console.log('Response from web app:', response.data);
    res.status(200).send('Notification processed');

  } catch (error) {
    console.error('Error in Cloud Function:', error);
    if (error.response) {
      console.error('Web app response:', error.response.data);
      console.error('Web app status:', error.response.status);
    }
    res.status(500).send('Internal Server Error');
  }
};

二、初始错误日志

  • Cloud Function错误:AxiosError: Request failed with status code 401 at settle
  • WebApp响应:
<HTML> <HEAD> <TITLE>Unauthorized</TITLE> </HEAD> <BODY BGCOLOR="#FFFFFF" TEXT="#000000"> <!-- GSE Default Error --> <H1>Unauthorized</H1> <H2>Error 401</H2> </BODY> </HTML>

三、后续测试与进展

调整后问题未完全解决:用已登录用户的令牌通过Postman调用WebApp成功,但服务账号的令牌仍失败。

测试详情

  1. 服务账号调整
    • 添加https://www.googleapis.com/auth/drive权限范围
    • 给服务账号授予Google App Script的编辑权限
    • 结果:出现500错误
  2. 已登录用户令牌测试
    • 用户拥有脚本编辑权限
    • 获取用户访问令牌后通过Postman调用
    • 结果:成功(之前失败,添加drive权限范围后修复)
  3. 服务账号全权限测试
    • 为服务账号添加WebApp所需的全部权限范围
    • 结果:还是500错误
  4. 切换WebApp部署配置测试
    • 部署为“执行身份:我自己 + 访问权限:任何人”
    • 用服务账号测试:成功,但我不能用这个配置

假设与令牌对比

  • 测试2和4说明Cloud Function和doPost函数本身没问题
  • 猜测服务账号可能不被WebApp的“拥有谷歌账号的任何人”规则认可

对比两种令牌信息:

服务账号令牌

{
  "issued_to": "some long number X",
  "audience": "same long number X",
  "user_id": "same long number X",
  "scope": "与已登录用户一致,仅缺少https://www.google.com/calendar/feeds(不影响WebApp调用)",
  "expires_in": 3395,
  "email": "xxxxxxxxxxx-compute@developer.gserviceaccount.com",
  "verified_email": true,
  "access_type": "online"
}

已登录用户令牌

{
  "issued_to": "KeyXYZ.apps.googleusercontent.com",
  "audience": "KeyXYZ.apps.googleusercontent.com",
  "user_id": "some long number Y",
  "scope": "与服务账号一致,仅包含https://www.google.com/calendar/feeds",
  "expires_in": 3575,
  "email": "myemail@gmail.com",
  "verified_email": true,
  "access_type": "offline"
}

解决方案建议

核心原因

服务账号默认不属于“拥有谷歌账号的任何人”范畴——WebApp的这个权限规则针对的是普通谷歌个人账户,服务账号是机器身份,即使验证通过也会被拦截,甚至触发500错误。

可行解决方法

  1. 使用服务账号模拟域用户(仅Google Workspace环境)
    如果你用的是Google Workspace域账号,可以开启域范围委派:

    • 在Google Admin控制台找到目标服务账号,开启域范围委派
    • 添加权限范围:https://www.googleapis.com/auth/script.external_request、https://www.googleapis.com/auth/drive
    • 修改Cloud Function的授权配置,模拟域内有WebApp访问权限的用户:
      const auth = new GoogleAuth({
        credentials: JSON.parse(process.env.SERVICE_ACCOUNT_KEY),
        scopes: ['https://www.googleapis.com/auth/script.external_request', 'https://www.googleapis.com/auth/drive'],
        subject: 'domain-user@your-domain.com' // 替换为域内用户邮箱
      });
      
  2. 自定义WebApp权限验证逻辑
    放弃WebApp默认权限配置,在doPost函数中自行验证服务账号令牌:

    • 修改WebApp的doPost函数,提取并验证请求头中的令牌
    • 示例代码:
      function doPost(e) {
        const authHeader = e.headers.Authorization;
        if (!authHeader || !authHeader.startsWith('Bearer ')) {
          return ContentService.createTextOutput('Unauthorized').setStatusCode(401);
        }
        const token = authHeader.split(' ')[1];
        const tokenInfo = JSON.parse(UrlFetchApp.fetch(`https://www.googleapis.com/oauth2/v1/tokeninfo?access_token=${token}`).getContentText());
        // 替换为你的服务账号邮箱
        const allowedAccount = 'xxxxxxxxxxx-compute@developer.gserviceaccount.com';
        if (tokenInfo.email !== allowedAccount || !tokenInfo.verified_email) {
          return ContentService.createTextOutput('Unauthorized').setStatusCode(401);
        }
        return ContentService.createTextOutput("Hello world");
      }
      
    • 同时将WebApp部署权限改为“任何人,甚至匿名”,通过自定义逻辑控制访问
  3. 改用Google Apps Script API调用
    不直接调用WebApp,通过API执行脚本函数:

    • 确保服务账号拥有脚本编辑权限
    • 调整Cloud Function代码,调用scripts.run方法:
      const { google } = require('googleapis');
      const script = google.script('v1');
      
      // 在auth初始化后执行
      const apiResponse = await script.scripts.run({
        auth: client,
        scriptId: 'YOUR_SCRIPT_ID', // 替换为你的App Script ID
        requestBody: {
          function: 'yourTargetFunction', // 替换为要执行的函数名
          parameters: [requestBody] // 传递参数
        }
      });
      

内容的提问来源于stack exchange,提问作者HardaxX

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.14 08:55:55