如何安全实现外部服务器调用非公开Google Apps Script Web App
安全调用非公开Google Apps Script Web App的解决方案
方案一:基于服务账号的OAuth2客户端凭证验证
利用你已有的服务账号,通过client_credentials类型的OAuth2令牌实现无交互验证,同时限制Web App访问范围。
1. 关联Apps Script项目与目标GCP项目
- 打开Apps Script项目,进入项目设置,勾选「显示appsscript.json清单文件」
- 编辑
appsscript.json,添加必要的OAuth权限范围(根据业务需求调整):{ "oauthScopes": [ "https://www.googleapis.com/auth/script.external_request", "https://www.googleapis.com/auth/drive.readonly" // 需调用Drive API时添加 ], "runtimeVersion": "V8" } - 返回项目设置,找到「Google Cloud Platform (GCP) 项目」,点击「更改项目」,选择服务账号所在的GCP项目
2. 部署受限制的Web App
- 点击「部署」→「部署为Web应用」:
- 执行权限:选择「以部署者身份执行」(确保服务账号能触发Web App业务逻辑)
- 访问权限:选择「仅限特定用户」并添加服务账号邮箱(格式:
xxx@xxx.iam.gserviceaccount.com),或选择「我的组织内的任何人」(仅适用于Google Workspace账号) - 点击部署,记录Web App访问URL
3. Web App端添加令牌验证逻辑
在doPost(或doGet)函数中,验证传入的Bearer令牌是否来自合法服务账号:
function doPost(e) { // 提取Authorization头中的令牌 const authHeader = e.headers.Authorization; if (!authHeader || !authHeader.startsWith('Bearer ')) { return ContentService.createTextOutput('Unauthorized').setStatusCode(401); } const token = authHeader.split(' ')[1]; // 校验令牌合法性 try { const response = UrlFetchApp.fetch(`https://oauth2.googleapis.com/tokeninfo?id_token=${token}`); const tokenInfo = JSON.parse(response.getContentText()); // 验证令牌所属服务账号 const allowedServiceAccount = 'your-service-account@your-project.iam.gserviceaccount.com'; if (tokenInfo.email !== allowedServiceAccount) { return ContentService.createTextOutput('Forbidden').setStatusCode(403); } } catch (err) { return ContentService.createTextOutput('Invalid token').setStatusCode(401); } // 执行业务逻辑 return ContentService.createTextOutput('Success').setStatusCode(200); }
4. C#端实现令牌获取与调用
复用现有服务账号密钥,获取令牌并调用Web App:
using Google.Apis.Auth.OAuth2; using System.Net.Http; using System.Threading.Tasks; public async Task InvokeWebApp() { // 加载服务账号密钥文件 var credential = GoogleCredential.FromFile("service-account-key.json") .CreateScoped(new[] { "https://www.googleapis.com/auth/script.external_request" }); // 获取访问令牌 var accessToken = await credential.UnderlyingCredential.GetAccessTokenForRequestAsync(); // 发起请求 using var httpClient = new HttpClient(); httpClient.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken); var response = await httpClient.PostAsync("https://script.google.com/macros/s/your-web-app-id/exec", null); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); // 处理返回结果 }
方案二:无OAuth2的双重验证(IP白名单+API密钥)
若不想使用OAuth2,可通过IP限制+自定义API密钥实现安全访问,适合固定IP的服务器场景。
1. Web App端添加验证逻辑
function doPost(e) { // 1. 验证请求来源IP(替换为公司服务器IP/IP段) const allowedIps = ["192.168.1.100", "203.0.113.0/24"]; const clientIp = e.headers["X-Forwarded-For"]?.split(",")[0] || e.remoteAddress; const isIpAllowed = allowedIps.some(ip => { if (ip.includes("/")) { // 简化IP段验证,可按需完善 const [baseIp, mask] = ip.split("/"); return clientIp.startsWith(baseIp.split(".").slice(0, mask/8).join(".")); } return clientIp === ip; }); if (!isIpAllowed) { return ContentService.createTextOutput("Forbidden: Invalid IP").setStatusCode(403); } // 2. 验证自定义API密钥 const validApiKey = "your-secret-random-api-key"; // 自行生成并妥善保管 const incomingApiKey = e.headers["X-API-Key"] || e.parameter.apiKey; if (incomingApiKey !== validApiKey) { return ContentService.createTextOutput("Unauthorized: Invalid API Key").setStatusCode(401); } // 执行业务逻辑 return ContentService.createTextOutput("Success").setStatusCode(200); }
2. C#端调用实现
在请求头中携带API密钥:
using System.Net.Http; using System.Threading.Tasks; public async Task InvokeWebApp() { using var httpClient = new HttpClient(); // 添加API密钥到请求头 httpClient.DefaultRequestHeaders.Add("X-API-Key", "your-secret-random-api-key"); var response = await httpClient.PostAsync("https://script.google.com/macros/s/your-web-app-id/exec", null); response.EnsureSuccessStatusCode(); var result = await response.Content.ReadAsStringAsync(); // 处理返回结果 }
关键注意事项
- 方案一中,需在GCP项目中启用Apps Script API
- 服务账号需被添加到Web App的允许访问列表中
- 方案二中,API密钥需通过环境变量或配置文件管理,禁止硬编码
- 若公司服务器使用动态IP,需改用静态IP或调整IP白名单策略
内容的提问来源于stack exchange,提问作者Paul
相关产品推荐
相关产品推荐

