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

使用Go通过服务账户下载Google Calendar事件附件遇登录重定向问题

问题:服务账户下载Google Calendar事件附件时遭遇登录重定向

我一直使用Google Calendar跟踪事件,需要获取这些事件并下载关联的附件,当前采用Go语言开发,也接受其他可行方案。

已尝试的操作步骤

  1. 使用账号A在网页版Google Calendar中创建带有Google Drive附件的事件
    新建日历邀请

  2. 读取账号B(Google服务账户)的JSON凭证文件,并创建google.HTTPClient
    凭证文件内容:

{
"type": "service_account",
"project_id": "GoogleProject",
"private_key_id": "...",
"private_key": "-----BEGIN PRIVATE KEY-----...\n-----END PRIVATE KEY-----\n",
"client_email": "ServiceAccount@GoogleProject.iam.gserviceaccount.com",
"client_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_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/ServiceAccount%40GoogleProject.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

创建客户端的Go代码:

import (
    "context"
    "log"
    "os"
    "time"
    gCalendar "google.golang.org/api/calendar/v3"
    gDrive "google.golang.org/api/drive/v3"
    "google.golang.org/api/option"
    "golang.org/x/oauth2/jwt"
)

var googleCalendarID = "..." // 目标日历ID

func main() {
    googleCredentialsJson, err := os.ReadFile("google_application_credentials.json")
    if err != nil || len(googleCredentialsJson) == 0 {
        log.Fatalf("读取凭证文件失败: %v", err)
    } 

    googleJWTConf, err := jwt.ConfigFromJSON(googleCredentialsJson,
        gCalendar.CalendarScope,
        gCalendar.CalendarEventsScope,
        gDrive.DriveScope,
        "https://www.googleapis.com/auth/cloud-platform",
    )
    if err != nil {
        log.Fatalf("加载Google凭证失败: %+v", err)
    }

    googleCtx := context.Background()
    googleHTTPClient := googleJWTConf.Client(googleCtx)
}
  1. 通过google.HTTPClient获取日历事件:
googleCalendarService, err := gCalendar.NewService(googleCtx, option.WithHTTPClient(googleHTTPClient))
if err != nil {
    log.Fatalf("初始化Calendar服务失败: %+v", err)
}

calEvents, err := googleCalendarService.Events.
    List(googleCalendarID).
    TimeMin(time.Now().Format(time.RFC3339)).
    TimeMax(time.Now().Add(24 * 30 * time.Hour).Format(time.RFC3339)).
    Context(googleCtx).Do()
if err != nil {
    log.Fatalf("获取日历事件失败: %v", err)
}    
  1. 尝试使用日历事件响应中的FileUrl下载附件:
import (
    "net/http"
    "net/http/httputil"
)

if len(calEvents.Items) > 0 {
    calEvent := calEvents.Items[0]
    if len(calEvent.Attachments) > 0 {
        rFileUrl := calEvent.Attachments[0].FileUrl

        request, err := http.NewRequestWithContext(googleCtx, "GET", rFileUrl, nil)
        if err != nil {
            log.Fatalf("创建请求失败: %v", err)
        }

        response, err := googleHTTPClient.Do(request)
        if err != nil {
            log.Fatalf("请求文件失败: %v", err)
        }
        defer response.Body.Close()

        if response.StatusCode != http.StatusOK || response.Header.Get("X-Frame-Options") == "DENY" {
            tmpDump, _ := httputil.DumpResponse(response, false)
            log.Errorf("响应异常: %s", string(tmpDump))
            log.Fatalf("响应状态码: %v", response.StatusCode)
        }
    }
}

遇到的问题

每次请求都会被重定向到Google登录页面,即使文件设置为公开也是如此(但未登录Google的浏览器会话可以通过同一URL访问该文件)。现有资料大多围绕个人Drive文件下载,Calendar附件相关内容极少,也不清楚附件的具体存储机制,看起来文件加载前存在多次重定向。

解决方案

问题根源在于直接使用FileUrl请求——该链接是面向普通用户的网页地址,而非API调用端点,服务账户的OAuth凭证无法直接用于这类网页请求。正确的做法是通过Google Drive API,利用附件的FileId来下载文件:

修正后的下载代码

import "io"

// 在获取到calEvents之后执行
if len(calEvents.Items) > 0 {
    calEvent := calEvents.Items[0]
    if len(calEvent.Attachments) > 0 {
        fileId := calEvent.Attachments[0].FileId // 提取文件ID,而非FileUrl
        
        // 初始化Drive服务
        driveService, err := gDrive.NewService(googleCtx, option.WithHTTPClient(googleHTTPClient))
        if err != nil {
            log.Fatalf("初始化Drive服务失败: %v", err)
        }

        // 请求下载文件
        resp, err := driveService.Files.Get(fileId).Download()
        if err != nil {
            log.Fatalf("下载文件失败: %v", err)
        }
        defer resp.Body.Close()

        // 将文件内容保存到本地
        outFile, err := os.Create("downloaded_attachment") // 根据文件类型修改后缀,如.pdf/.docx
        if err != nil {
            log.Fatalf("创建本地文件失败: %v", err)
        }
        defer outFile.Close()

        _, err = io.Copy(outFile, resp.Body)
        if err != nil {
            log.Fatalf("保存文件内容失败: %v", err)
        }
        log.Println("附件下载成功")
    }
}

关键注意事项

  1. 权限配置:必须将目标Drive文件共享给服务账户的邮箱(即凭证中的client_email字段值),权限设置为「查看者」即可。
  2. API权限校验:确保JWT配置中包含gDrive.DriveScope(你的代码已包含此配置,无需修改)。
  3. 文件ID的可靠性:Calendar事件附件的FileId直接对应Drive中的文件ID,是API调用的唯一标识,比FileUrl更稳定可靠。

内容的提问来源于stack exchange,提问作者The Kaese

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 19:16:03