Google Drive API上传身份异常:部分用户操作以开发者账号执行
问题排查请求:Google Drive/Gmail API工具打包后异常
我开发的Python3工具原本支持用户上传PDF到团队Google Drive,并发送内部审核邮件。自本周一起,约1/3到1/2的用户出现异常:上传和邮件操作以我的Google账号执行,还偶尔抛出HttpError 403 when requesting https://gmail.googleapis.com/gmail/v1/users/username/messages/send?alt=json returned 'Delegation denied for 'my.email@email.com'错误。工具通过PyInstaller打包为exe分发,打包命令是pyinstaller --onefile path/to/code.py。
已尝试的排查步骤
- 以管理员身份运行exe
- 重启用户电脑
- 直接运行Python源码文件可正常工作
- 移除PyInstaller打包时的
--hidden-import='googleapiclient'参数 - 将邮件模块中的
userId='me'改为用户实际邮箱地址
相关代码
授权范围与凭证配置
self.SCOPES = ['https://www.googleapis.com/auth/gmail.modify', 'https://www.googleapis.com/auth/spreadsheets', 'https://www.googleapis.com/auth/drive']
credentials.json内容:
{ "installed":{ "client_id":"client_id.apps.googleusercontent.com", "project_id":"project_ID", "auth_uri":"https://accounts.google.com/o/oauth2/auth", "token_uri":"https://oauth2.googleapis.com/token", "auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs", "client_secret":"secret_code", "redirect_uris":[ "urn:ietf:wg:oauth:2.0:oob", "http://localhost" ] } }
上传核心函数
def upload(self, letterfile, plansfile, parentdrive): # Check for json credentials for google drive and API access try: x = os.stat(os.path.join(self.prefs['data'], 'token.json')) result = time.time() - x.st_mtime if result > 129600: os.remove(os.path.join(self.prefs['data'], 'token.json')) except: pass creds = None jsonpath = os.path.join(self.prefs['data'], 'token.json') if os.path.exists(jsonpath): creds = Credentials.from_authorized_user_file(jsonpath, self.SCOPES) # If there are no (valid) credentials available, let the user log in. try: if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file( os.path.join(self.prefs['data'], 'credentials.json'), self.SCOPES) creds = flow.run_local_server(port=0) # Save the credentials for the next run with open(os.path.join(self.prefs['data'], 'token.json'), 'w') as token: token.write(creds.to_json()) except BaseException as e: print(f'Error with credentials.json check: {e}') # Build the services to access the Gmail API and Drive API driveService = build('drive', 'v3', credentials=creds) file = letterfile folderLink = f'https://drive.google.com/drive/folders/{parentdrive}' # Work with different attachment formats (Text, Image, or PDF). This # will need to be a FOR loop to iterate through all imported files # from the app content_type, encoding = mimetypes.guess_type(file) if content_type is None or encoding is not None: content_type = 'application/octet-stream' main_type, sub_type = content_type.split('/', 1) if main_type == 'text': with open(file, 'rb') as fp: msg = MIMEText(fp.read(), _subtype=sub_type) fp.close() elif main_type == 'image': with open(file, 'rb') as fp: msg = MIMEImage(fp.read(), _subtype=sub_type) fp.close() else: with open(file, 'rb') as fp: msg = MIMEBase(main_type, sub_type) msg.set_payload(fp.read()) fp.close() calcfile = os.path.basename(letterfile) try: planname = os.path.basename(plansfile) except BaseException as e: planname = [] for planfile in plansfile: planname.append(os.path.basename(planfile)) # Upload file to Drive Project Folder file_metadata = {'name': calcfile, 'parents': [parentdrive]} media = MediaFileUpload(letterfile, mimetype=content_type, resumable=True) tempcalcfile = driveService.files().create(body=file_metadata, media_body=media, supportsAllDrives=True).execute() try: plan_metadata = {'name': planname, 'parents': [parentdrive]} media = MediaFileUpload(plansfile, mimetype=content_type, resumable=True) tempplansfile = driveService.files().create(body=plan_metadata, media_body=media, supportsAllDrives=True).execute() except BaseException as e: for i in range(len(planname)): plan_metadata = {'name': planname[i], 'parents': [parentdrive]} media = MediaFileUpload(plansfile[i], mimetype=content_type, resumable=True) tempplansfile = driveService.files().create(body=plan_metadata, media_body=media, supportsAllDrives=True).execute() print('Files sent to Drive Folder through API')
排查建议
- 检查token.json的分发问题:打包后的exe可能意外包含了你本地的
token.json(存储着你的账号授权信息),导致部分用户直接复用你的凭证。确认打包时是否排除了token.json,并确保工具首次运行时强制用户完成自己的OAuth授权流程。 - 验证OAuth应用类型:当前使用的是
installed类型的OAuth客户端,适合单用户桌面应用。多用户团队场景下,建议改用Web应用类型,或启用域范围委派(如果是Google Workspace域),避免用户间凭证冲突。 - 排查PyInstaller路径问题:
--onefile打包模式下,运行时会解压到临时目录,工具读取credentials.json和token.json的路径可能与源码运行时不一致。添加日志输出凭证文件的实际路径,确认用户端是否在正确位置生成/读取文件,防止多用户共享同一凭证。 - 修复凭证过期逻辑:当前删除过期token的逻辑(超过1.5天自动删除)可能存在漏洞,比如文件权限导致无法删除旧token,或
os.stat抛出异常后直接跳过,导致无效token被复用。添加日志记录token检查过程,确保过期token被正确清理,触发用户重新授权。 - 检查Gmail API授权权限:
Delegation denied错误通常与权限委派有关,确认你的账号是否被授权代表其他用户发送邮件,或用户Gmail账号是否允许第三方应用访问。如果使用域范围委派,需在Google Admin控制台为服务账号授权对应API范围。 - 确保依赖完整打包:PyInstaller可能未正确打包Google API相关依赖,尝试添加
--hidden-import='googleapiclient.discovery'、--hidden-import='google_auth_oauthlib.flow'等参数,或使用--collect-all googleapiclient确保所有相关模块被打包。
内容的提问来源于stack exchange,提问作者Ryan
相关产品推荐
相关产品推荐

