Google Apps Script Web App 为何提示无法打开文件
结论
当 Execute as: Me 与 SpreadsheetApp.openById() 配合使用时,Google Sheet 的读写操作以部署者身份执行。访问者通常无须获得该表格的共享权限,也不应通过共享表格来解决 Web App 的访问问题。
当前错误更可能出在 Web App 入口的账号识别或部署授权阶段。以下现象都指向入口问题:
- Incognito 或干净会话可以正常访问。
- 登录多个账号的浏览器更容易报错。
- 页面显示 Google Drive 的“无法打开文件”,没有出现 Apps Script 执行异常。
还要注意,当 Web App 允许匿名访问并以部署者身份运行时,Session.getActiveUser().getEmail() 经常返回空字符串,因此不能用它为任意 Google 账号提供通用身份认证。
Execute as: Me 实际控制什么
Web App 的访问过程有两层。
第一层是入口访问。Google 会判断当前浏览器是否有权打开这个 Apps Script 部署。判断结果受以下因素影响:
Who has access的设置- Google 当前选中的账号
- Google Workspace 域策略
- 部署是否仍然有效
- 用户打开的是否为当前部署的
/execURL
第二层是脚本执行。请求进入 doGet()、doPost() 或 google.script.run 后,Execute as: Me 决定服务器端以谁的权限访问 Google 服务。
例如:
SpreadsheetApp.openById("MY_SHEET_ID")
在“Execute as: Me”部署中,这段代码使用部署者的授权,访问者无须拥有该表格的查看或编辑权限。
如果失败请求没有出现在 Apps Script 的 Executions 页面中,请求就尚未进入脚本,此时应从部署入口排查,而不是检查 openById()。
为什么多账号登录时容易出现这个错误
Google 登录会话可以同时保存多个账号。访问 /exec 地址时,Google 有时会选择另一个账号完成入口授权。这个账号可能不属于公司域,也可能因 Workspace 策略而无法访问该部署。
从 URL 中删除 /u/0 或添加 /a/example.com/,只能影响账号或域的路由选择,无法改变部署权限,也无法绕过 Workspace 管理策略。这些 URL 调整在不同浏览器会话中的表现并不稳定,不适合作为正式解决方案。
如果 Incognito 可以正常运行,通常说明代码和表格权限没有根本问题。普通浏览器中的账号选择、Cookie 或登录状态更可能导致入口路由异常。
先确认错误发生在哪一层
可以暂时将 doGet() 简化为:
function doGet() {
return HtmlService.createHtmlOutput("Web App is running.");
}
部署一个新版本,然后让遇到问题的用户访问新的 /exec URL。
打开 Apps Script 左侧的 Executions,根据结果继续排查:
- 有执行记录且执行失败:查看具体异常、函数名和堆栈。
- 完全没有执行记录:请求被挡在部署入口,应检查账号选择、部署访问范围和 Workspace 管理策略。
- 简化后的页面正常,恢复表格代码后失败:检查 Sheet ID、部署者是否拥有文件及相关 OAuth 授权。
- 只有某个浏览器配置失败:使用单独的浏览器 Profile 登录目标账号,比修改
/u/0更可靠。
每次修改代码后,还要在 Manage deployments 中编辑部署并选择新版本。只保存代码不会自动更新现有的版本化部署。
同一 Workspace 域内的推荐方案
如果所有访问者都属于同一个 Google Workspace 域,可以使用以下配置:
Execute as: MeWho has access: 组织内用户,具体名称以部署界面显示为准- Google Sheet:仅共享给部署者或后台维护账号
- 在脚本中使用 Email 白名单限制具体人员
示例:
const ALLOWED_EMAILS = new Set([
"myemail1@domain.com",
"myemail2@domain.com",
]);
function getCurrentUserEmail_() {
return Session.getActiveUser()
.getEmail()
.trim()
.toLowerCase();
}
function assertAllowedUser_() {
const email = getCurrentUserEmail_();
if (!email) {
throw new Error(
"无法识别当前 Google 账号。请使用组织账号登录后重新打开此应用。"
);
}
if (!ALLOWED_EMAILS.has(email)) {
throw new Error("Access denied.");
}
return email;
}
function doGet() {
try {
assertAllowedUser_();
return HtmlService
.createHtmlOutputFromFile("index")
.setTitle("LARK System BSS SKP");
} catch (error) {
return HtmlService.createHtmlOutput(
`<p>${escapeHtml_(error.message)}</p>`
);
}
}
function readSheetData() {
assertAllowedUser_();
const spreadsheet = SpreadsheetApp.openById("MY_SHEET_ID");
const sheet = spreadsheet.getSheetByName("Sheet1");
if (!sheet) {
throw new Error('找不到工作表 "Sheet1"。');
}
return sheet.getDataRange().getDisplayValues();
}
function writeSheetValue(row, column, value) {
assertAllowedUser_();
const spreadsheet = SpreadsheetApp.openById("MY_SHEET_ID");
const sheet = spreadsheet.getSheetByName("Sheet1");
if (!sheet) {
throw new Error('找不到工作表 "Sheet1"。');
}
sheet.getRange(row, column).setValue(value);
}
function escapeHtml_(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """)
.replaceAll("'", "'");
}
权限检查不能只放在 doGet() 中。读取、写入和删除数据的每个服务器端入口都要再次执行 assertAllowedUser_(),包括通过 google.script.run 调用的函数。
原代码中的这一段也需要调整:
HtmlService.createHtmlOutput(
"Access Denied: You are not authorized to use this app.",
userEmail
);
HtmlService.createHtmlOutput() 不支持通过第二个参数附加 Email。应先生成完整字符串,或只传入固定的错误信息。
为什么 Who has access: Anyone 与 Email 白名单可能冲突
如果 Anyone 允许未登录用户访问,Google 就无须向脚本提供访问者身份。此时下面的调用可能返回空字符串:
Session.getActiveUser().getEmail()
这里缺失的是可靠的当前用户 Email,与 openById() 的权限无关。
因此,以下组合无法稳定实现“任意 Google 账号登录后,再按 Email 白名单放行”:
- Web App 允许匿名访问
- 脚本以部署者身份运行
- 使用
Session.getActiveUser().getEmail()识别外部用户
也不能让前端通过查询参数、表单字段或 JavaScript 变量提交 Email,例如:
/exec?email=user@example.com
用户可以随意修改这类值,所以它不能用于证明身份。
外部用户也需要访问时怎么办
如果白名单包含公司域外的 Google 账号,先确认当前部署选项是否要求登录,并测试 Session.getActiveUser().getEmail() 在该部署和域策略下能否返回外部用户的 Email。部署界面显示 Anyone,不代表脚本一定能够取得用户身份。
通常有两种处理方式。
使用 Google 登录并在服务端验证身份令牌
Web App 可以继续将底层 Sheet 设为私有。前端要求用户通过 Google Identity Services 登录,再将 Google ID token 交给受信任的服务端验证。服务端至少要检查:
- 令牌签名
aud是否等于本应用的 OAuth Client IDiss是否为受信任的 Google 签发方- 令牌是否过期
email_verified是否为真email是否在白名单内- 如果限制 Workspace 域,还要检查相应的域声明
验证通过后,再由拥有 Sheet 权限的后端执行读写。不要只在浏览器端解析 JWT,也不要信任客户端直接提交的 Email。
这套方案比依赖 Session.getActiveUser() 更复杂。对于处理敏感数据的应用,通常更适合将认证和数据接口放在 Cloud Run 等正式后端中,并使用服务账号或受控账号访问数据。
将应用限制在同一 Workspace 域
如果业务不需要支持外部账号,可以把部署访问范围限制在公司域内,再配合 Session.getActiveUser().getEmail() 和白名单使用。这样配置更简单,身份来源也更明确。
如果部署界面中没有预期的组织访问选项,或外部用户始终在入口处被拒绝,应让 Workspace 管理员检查组织策略。脚本代码无法绕过管理员设置的应用、外部访问或授权限制。
建议的排查顺序
- 从 Manage deployments 重新复制当前部署的
/execURL。 - 用只返回静态文本的
doGet()创建新版本并测试。 - 失败后立即查看 Executions,确认请求是否进入脚本。
- 使用只登录目标账号的浏览器 Profile 测试,避免多账号会话干扰。
- 确认部署者本人仍拥有目标 Sheet,并核对
MY_SHEET_ID。 - 确认用户属于部署允许的账号或 Workspace 范围。
- 检查
Session.getActiveUser().getEmail()是否为空。 - 如果请求没有进入脚本,让 Workspace 管理员检查组织策略。
- 如果需要支持外部账号,改用经过服务端验证的登录机制。
只要请求已经进入以部署者身份运行的脚本,访问者通常无须拥有任何 Google Sheet 权限。共享底层 Sheet 无法解决入口处的账号路由或域策略问题,还会破坏原有的权限隔离设计。
备注:内容仅供参考。
