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

Google Apps Script向后端发送请求的身份验证实现问题咨询

Google Apps Script向后端发送请求的身份验证实现问题咨询

你遇到的「Wrong number of segments in token」报错其实是令牌类型不匹配导致的——你在GAS里用ScriptApp.getOAuthToken()拿到的是OAuth 2.0访问令牌,但后端用verifyIdToken()验证的是ID Token(JWT格式),这俩完全不是一回事:访问令牌不一定是三段式的JWT,自然会报段数错误。

下面给你两种解决方案,先快速修复现有代码,再实现你偏好的服务账号验证方案:


方案一:快速修复——改用Identity Token(ID Token)验证

这个方案只需要调整GAS的令牌获取方式,后端代码基本不用改:

1. 补充GAS清单的权限范围

打开GAS编辑器的appsscript.json清单文件,在oauthScopes里添加身份令牌所需的权限:

"oauthScopes": [
  "openid",
  "https://www.googleapis.com/auth/script.external_request",
  "https://www.googleapis.com/auth/cloud-platform",
  "https://www.googleapis.com/auth/spreadsheets.currentonly",
  "https://www.googleapis.com/auth/script.identity" // 新增这个权限
]

2. 修改GAS的请求代码

把ScriptApp.getOAuthToken()换成ScriptApp.getIdentityToken(),必须传入后端的OAuth2客户端ID作为受众(audience),这样生成的ID Token的aud字段才会和后端的CLIENT_ID匹配:

async function syncWithBackend(payload) {
  const ui = SpreadsheetApp.getUi();
  const BACKEND_CLIENT_ID = "你的后端OAuth2客户端ID"; // 和后端代码里的CLIENT_ID保持一致

  const options = {
    method: "post",
    headers: { 
      "Content-Type": "application/json",
      Authorization: `Bearer ${ScriptApp.getIdentityToken(BACKEND_CLIENT_ID)}` // 改用Identity Token并指定受众
    },
    payload: JSON.stringify(payload),
  };

  try {
    const res = UrlFetchApp.fetch('backend-url', options);
    const responseText = res.getContentText();
    ui.alert('Response from backend: ' + responseText); 
  } catch (error) {
    ui.alert('Error communicating with backend: ' + error.message);
  }
}

3. 后端验证保持不变

你现有的后端verifyGoogleOAuthToken代码可以直接用,只要CLIENT_ID是后端的OAuth2客户端ID即可——现在传入的是标准JWT格式的ID Token,verifyIdToken()能正常解析验证。


方案二:使用服务账号验证(你偏好的方式)

如果想更严谨地验证请求来自绑定的GCP项目,可以用GCP服务账号签名请求,后端验证服务账号的身份:

步骤1:准备GCP服务账号

  1. 打开GAS绑定的GCP项目,进入「IAM与管理」→「服务账号」
  2. 创建或选择一个服务账号,下载其JSON密钥文件
  3. 把密钥文件的内容存储到GAS的脚本属性中(不要硬编码!):
    • GAS编辑器→「文件」→「项目属性」→「脚本属性」→ 添加键SERVICE_ACCOUNT_KEY,值为密钥文件的JSON字符串

步骤2:GAS中生成服务账号JWT

编写函数生成服务账号签名的JWT,代替原有的令牌:

function generateServiceAccountJwt() {
  // 从脚本属性获取服务账号密钥
  const serviceAccountKey = JSON.parse(PropertiesService.getScriptProperties().getProperty('SERVICE_ACCOUNT_KEY'));
  const now = Math.floor(Date.now() / 1000);
  const expiry = now + 3600; // 令牌有效期1小时

  // 构建JWT的Header和Payload
  const jwtHeader = JSON.stringify({ alg: 'RS256', typ: 'JWT' });
  const jwtPayload = JSON.stringify({
    iss: serviceAccountKey.client_email, // 服务账号邮箱
    aud: '你的后端URL或验证受众', // 后端验证时的匹配字段
    exp: expiry,
    iat: now
  });

  // 编码并生成签名
  const encodedHeader = Utilities.base64EncodeWebSafe(jwtHeader);
  const encodedPayload = Utilities.base64EncodeWebSafe(jwtPayload);
  const signatureInput = `${encodedHeader}.${encodedPayload}`;
  const signature = Utilities.computeRsaSha256Signature(signatureInput, serviceAccountKey.private_key);
  const encodedSignature = Utilities.base64EncodeWebSafe(signature);

  return `${encodedHeader}.${encodedPayload}.${encodedSignature}`;
}

// 修改同步函数使用这个JWT
async function syncWithBackend(payload) {
  const ui = SpreadsheetApp.getUi();
  const options = {
    method: "post",
    headers: { 
      "Content-Type": "application/json",
      Authorization: `Bearer ${generateServiceAccountJwt()}`
    },
    payload: JSON.stringify(payload),
  };

  try {
    const res = UrlFetchApp.fetch('backend-url', options);
    const responseText = res.getContentText();
    ui.alert('Response from backend: ' + responseText); 
  } catch (error) {
    ui.alert('Error communicating with backend: ' + error.message);
  }
}

步骤3:后端验证服务账号JWT

可以用Google的OAuth2客户端库直接验证,或者用JWT库验证签名:

import { OAuth2Client } from 'google-auth-library';
const ALLOWED_SERVICE_ACCOUNT = "你的允许的服务账号邮箱";
const AUDIENCE = "你的后端URL或验证受众";

export async function verifyServiceAccountJwt(token: string) {
  const client = new OAuth2Client();
  const ticket = await client.verifyIdToken({
    idToken: token,
    audience: AUDIENCE,
  });
  const payload = ticket.getPayload();

  // 验证是否为允许的服务账号
  if (payload.iss !== ALLOWED_SERVICE_ACCOUNT) {
    throw new Error("Unauthorized service account");
  }
  return payload;
}

安全注意事项

  • 永远不要在GAS代码中硬编码密钥、客户端ID等敏感信息,一律用「脚本属性」存储
  • 遵循最小权限原则:GAS的oauthScopes只请求必要的权限,服务账号只分配必要的IAM角色
  • 后端验证时,除了令牌有效性,还要额外验证iss(签发者)、aud(受众)字段,确保只有指定身份能访问

备注:内容来源于stack exchange,提问作者itsDanial

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.14 16:19:31