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服务账号
- 打开GAS绑定的GCP项目,进入「IAM与管理」→「服务账号」
- 创建或选择一个服务账号,下载其JSON密钥文件
- 把密钥文件的内容存储到GAS的脚本属性中(不要硬编码!):
- GAS编辑器→「文件」→「项目属性」→「脚本属性」→ 添加键
SERVICE_ACCOUNT_KEY,值为密钥文件的JSON字符串
- GAS编辑器→「文件」→「项目属性」→「脚本属性」→ 添加键
步骤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
相关产品推荐
相关产品推荐

