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

Google Chat API服务账号上传附件遇403:不支持应用认证

Google Chat API 附件上传403问题排查与解答

问题描述

使用服务账号集成Google Chat API时,纯文本消息发送正常,但调用attachments:upload接口上传附件时收到403错误,提示该方法不支持应用认证。

可行操作

服务账号发送纯文本消息的代码可正常运行:

public JSONObject sendMessage(String spaceName, String text) {
    JSONObject body = new JSONObject();
    body.put("text", text);
    return post(chatBase + spaceName + "/messages", body.toString(), getAccessToken());
}

失败场景

调用可恢复上传API上传附件时的错误信息:

ai.knolli.googleChat.services.GoogleChatService$GoogleChatException: uploadAttachment initiation failed [HTTP 403]: {
  "error": {
    "code": 403,
    "message": "This method doesn't support app authentication with a service account. Authenticate with a user account.",
    "status": "PERMISSION_DENIED",
    "details": [
      {
        "@type": "type.googleapis.com/google.rpc.Help",
        "links": [
          {
            "description": "See article Authenticate as a user.",
            "url": "https://developers.google.com/chat/api/guides/auth/users"
          }
        ]
      }
    ]
  }
}

完整代码

@Service
public class GoogleChatService {

    @Value("${google.chat.service-account-path}")
    private String serviceAccountPath;

    @Value("${google.chat.api-base:https://chat.googleapis.com/v1/}")
    private String chatBase;

    @Value("${google.chat.impersonated-user:}")
    private String impersonatedUser;

    private static final List<String> SCOPES = List.of(
        "https://www.googleapis.com/auth/chat.bot",
        "https://www.googleapis.com/auth/chat.messages",
        "https://www.googleapis.com/auth/chat.messages.create",
        "https://www.googleapis.com/auth/chat.spaces",
        "https://www.googleapis.com/auth/chat.spaces.readonly",
        "https://www.googleapis.com/auth/chat.app.memberships",
        "https://www.googleapis.com/auth/chat.spaces.create",
        "https://www.googleapis.com/auth/chat.memberships.app"
    );

    private HttpClient httpClient;
    private GoogleCredentials baseCredentials;

    @PostConstruct
    public void init() {
        this.httpClient = HttpClient.newHttpClient();
        try (InputStream in = new FileInputStream(serviceAccountPath)) {
            this.baseCredentials = GoogleCredentials.fromStream(in);
        } catch (Exception e) {
            throw new GoogleChatException("Failed to load service account from: " + serviceAccountPath, e);
        }
    }

    private String getAccessToken() {
        try {
            GoogleCredentials creds = baseCredentials.createScoped(SCOPES);
            if (impersonatedUser != null && !impersonatedUser.isBlank()) {
                creds = creds.createDelegated(impersonatedUser);
            }
            creds.refreshIfExpired();
            return creds.getAccessToken().getTokenValue();
        } catch (Exception e) {
            throw new GoogleChatException("Failed to get access token", e);
        }
    }

    public JSONObject sendMessage(String spaceName, String text) {
        JSONObject body = new JSONObject();
        body.put("text", text);
        return post(chatBase + spaceName + "/messages", body.toString(), getAccessToken());
    }

    public String uploadAttachment(String spaceName, File file) {
        String token    = getAccessToken();
        String mimeType = detectMimeType(file.getName());
        long fileSize   = file.length();

        String initiateUrl = chatBase.replace("/v1/", "/upload/v1/")
            + spaceName + "/attachments:upload?uploadType=resumable";

        try {
            HttpRequest initiateRequest = HttpRequest.newBuilder()
                .uri(URI.create(initiateUrl))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .header("X-Goog-Upload-Protocol", "resumable")
                .header("X-Goog-Upload-Command", "start")
                .header("X-Goog-Upload-Header-Content-Length", String.valueOf(fileSize))
                .header("X-Goog-Upload-Header-Content-Type", mimeType)
                .POST(HttpRequest.BodyPublishers.ofString(
                    new JSONObject().put("filename", file.getName()).toString()))
                .build();

            HttpResponse<String> initiateResponse =
                httpClient.send(initiateRequest, HttpResponse.BodyHandlers.ofString());

            if (initiateResponse.statusCode() != 200) {
                throw new GoogleChatException("uploadAttachment initiation failed [HTTP "
                    + initiateResponse.statusCode() + "]: " + initiateResponse.body());
            }

            String uploadUrl = initiateResponse.headers()
                .firstValue("x-goog-upload-url")
                .orElseThrow();

            HttpRequest uploadRequest = HttpRequest.newBuilder()
                .uri(URI.create(uploadUrl))
                .header("Content-Type", mimeType)
                .header("X-Goog-Upload-Offset", "0")
                .header("X-Goog-Upload-Command", "upload, finalize")
                .POST(HttpRequest.BodyPublishers.ofByteArray(readAllBytes(file)))
                .build();

            HttpResponse<String> uploadResponse =
                httpClient.send(uploadRequest, HttpResponse.BodyHandlers.ofString());

            return new JSONObject(uploadResponse.body())
                .getJSONObject("attachmentDataRef")
                .getString("attachmentUploadToken");

        } catch (Exception e) {
            throw new GoogleChatException("uploadAttachment failed", e);
        }
    }

    public JSONObject sendMessageWithAttachment(String spaceName, String text, File file) {
        String uploadToken = uploadAttachment(spaceName, file);

        JSONObject attachmentDataRef = new JSONObject();
        attachmentDataRef.put("attachmentUploadToken", uploadToken);

        JSONObject attachment = new JSONObject();
        attachment.put("attachmentDataRef", attachmentDataRef);

        JSONArray attachments = new JSONArray();
        attachments.put(attachment);

        JSONObject body = new JSONObject();
        body.put("text", text);
        body.put("attachment", attachments);

        return post(chatBase + spaceName + "/messages", body.toString(), getAccessToken());
    }

    private JSONObject post(String url, String jsonBody, String token) {
        try {
            HttpRequest request = HttpRequest.newBuilder()
                .uri(URI.create(url))
                .header("Authorization", "Bearer " + token)
                .header("Content-Type", "application/json")
                .POST(HttpRequest.BodyPublishers.ofString(jsonBody))
                .build();

            HttpResponse<String> response =
                httpClient.send(request, HttpResponse.BodyHandlers.ofString());

            return new JSONObject(response.body());
        } catch (Exception e) {
            throw new GoogleChatException("POST failed", e);
        }
    }

    private static String detectMimeType(String fileName) {
        return "application/octet-stream";
    }

    private static byte[] readAllBytes(File file) throws IOException {
        try (InputStream in = new FileInputStream(file)) {
            return in.readAllBytes();
        }
    }

    public static class GoogleChatException extends RuntimeException {
        public GoogleChatException(String message, Throwable cause) {
            super(message, cause);
        }
    }
}

技术问询与解答

问询1:在Google Chat中,是否无法使用service account(机器人认证)上传附件?

是的,Google Chat的attachments:upload接口明确不支持服务账号的机器人认证模式,仅允许通过用户身份认证调用。

问询2:上传附件是否必须使用用户OAuth(或带impersonation的全域委派)?

对,必须使用用户OAuth2认证,或者通过服务账号配置全域委派来模拟Google Workspace中的特定用户身份(即代码中impersonatedUser配置的账号)完成上传操作。

问询3:若上述问题答案为是,上传附件需要哪些具体的SCOPES?

完成附件上传与带附件消息发送,仅需https://www.googleapis.com/auth/chat.messages.create权限范围即可;如果需要管理消息的其他操作,也可以保留https://www.googleapis.com/auth/chat.messages。

问询4:是否存在通过机器人发送文件的替代方案?

有两种可行的替代方案:

  • 服务账号模拟用户:确保服务账号已在Google Workspace后台配置全域委派权限,且impersonatedUser为Workspace内的有效用户,同时该用户拥有目标Chat空间的访问权限,即可用模拟身份完成附件上传。
  • Drive链接间接发送:将文件上传至Google Drive并生成共享链接,然后用服务账号发送包含该链接的纯文本消息,用户点击链接即可访问文件。

内容的提问来源于stack exchange,提问作者Gaurav Kumar

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.01 17:37:26