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

Google Cloud Function v2调用Play Developer API返回401未授权错误(已配置管理员权限)

Google Cloud Function v2调用Play Developer API返回401未授权错误(已配置管理员权限)

我最近碰到过一模一样的问题:用Node.js/TypeScript写的Cloud Function v2处理Play Store的Pub/Sub通知,调用Play Developer API验证购买令牌时一直返回401 Unauthorized,明明GCP里的权限都配了,折腾半天才找到几个容易忽略的坑。先把你的问题场景和代码整理出来,再一步步说解决方案:

我的问题场景(和你完全一致)

  • Cloud Function v2通过Pub/Sub触发,处理Google Play的订阅通知
  • 调用androidpublisher.purchases.subscriptions.get时,始终报错:GaxiosError: The current user has insufficient permissions to perform the requested operation.
  • 已经在GCP IAM里给服务账号配置了Android Publisher角色,还是没用

我的代码结构(和你匹配)

import { onMessagePublished } from "firebase-functions/v2/pubsub";
import { setGlobalOptions } from "firebase-functions/v2";
import * as admin from "firebase-admin";
import { google, Auth } from "googleapis";

// Set global options for the function's region
setGlobalOptions({ region: "asia-southeast2" });

// Initialize Firebase Admin SDK
admin.initializeApp();
const firestore = admin.firestore();

// --- Configuration ---
const PUB_SUB_TOPIC = "play-store-notifications";
const PACKAGE_NAME = "com.example.myapp"; // Placeholder
const SUBSCRIPTION_ID = "premium_subscription"; // Placeholder
const SERVICE_ACCOUNT_EMAIL = "my-service-account@my-gcp-project.iam.gserviceaccount.com"; // Placeholder

// --- Auth Initialization ---
let authClient: Auth.GoogleAuth;
let androidpublisher: any;

const initializeAuth = async () => {
  try {
    const auth = new google.auth.GoogleAuth({
      scopes: ["https://www.googleapis.com/auth/androidpublisher"],
    });
    authClient = auth;
    androidpublisher = google.androidpublisher({
      version: "v3",
      auth: authClient,
    });
    console.log("Auth client and Android Publisher initialized successfully.");
  } catch (error) {
    console.error("Failed to initialize auth client:", error);
    throw new Error("Could not initialize Google Auth.");
  }
};

const authPromise = initializeAuth();

// --- Main Cloud Function ---
export const handlePlayStoreNotification = onMessagePublished(
  {
    topic: PUB_SUB_TOPIC,
    serviceAccount: SERVICE_ACCOUNT_EMAIL,
  },
  async (event) => {
    try {
      await authPromise;
      if (!androidpublisher) {
        console.error("Android Publisher client is not initialized. Aborting.");
        return;
      }
    } catch (error) {
      console.error("Auth initialization failed. Aborting.", error);
      return;
    }

    // 1. Decode the notification from Google Play
    let subscriptionNotification;
    try {
      const messageData = event.data.message.data
        ? Buffer.from(event.data.message.data, "base64").toString()
        : "{}";
      const parsedData = JSON.parse(messageData);
      subscriptionNotification = parsedData.subscriptionNotification;
      if (!subscriptionNotification) {
        console.error("Invalid or test notification format:", parsedData);
        return;
      }
    } catch (error) {
      console.error("Failed to parse notification:", error);
      return;
    }

    const { purchaseToken, notificationType } = subscriptionNotification;
    if (!purchaseToken) {
      console.info("Notification without a purchaseToken (likely a test).", subscriptionNotification);
      return;
    }

    // 2. Verify the purchase with the Google Play Developer API
    try {
      console.log(`Verifying purchase token starting with: ${purchaseToken.substring(0, 20)}...`);
      const purchase = await androidpublisher.purchases.subscriptions.get({
        packageName: PACKAGE_NAME,
        subscriptionId: SUBSCRIPTION_ID,
        token: purchaseToken,
      });
      console.log("Purchase verified successfully.");

      // 3. Find the user in Firestore via the purchase token
      const userQuery = await firestore
        .collection("users")
        .where("purchaseId", "==", purchaseToken)
        .limit(1)
        .get();

      if (userQuery.empty) {
        console.warn(`User not found for purchaseToken: ${purchaseToken}`);
        return;
      }

      const userDocRef = userQuery.docs[0].ref;

      // 4. Update the user's document in Firestore based on the notification type
      const expiryTimeMillis = purchase.data.expiryTimeMillis;
      const updateData: { [key: string]: unknown } = {};

      switch (notificationType) {
        case 4: // SUBSCRIPTION_RENEWED
        case 2: // SUBSCRIPTION_PURCHASED
          updateData.subscriptionLevel = "premium";
          if (expiryTimeMillis) {
            updateData.subscriptionExpiry = admin.firestore.Timestamp.fromMillis(parseInt(expiryTimeMillis, 10));
          }
          break;
        case 3: // SUBSCRIPTION_EXPIRED
          updateData.subscriptionLevel = "free";
          break;
      }

      // 执行Firestore更新逻辑(省略)
    } catch (error) {
      console.error("Failed to verify purchase:", error);
    }
  }
);

解决步骤(按优先级排序)

1. 重中之重:给服务账号授予Google Play开发者后台权限

这是90%的人踩的坑!GCP IAM里的Android Publisher角色完全不够,必须在Google Play开发者后台单独授权服务账号:

  • 打开你的Play开发者后台,进入「用户和权限」页面
  • 点击「邀请新用户」,输入代码里的SERVICE_ACCOUNT_EMAIL
  • 权限至少勾选「查看应用权限」+「管理订单和订阅权限」(可以先给「管理员」权限测试,确认后再缩窄)
  • 发送邀请后无需手动接受,服务账号是机器账号会自动关联

2. 确认Cloud Function的运行时服务账号正确

你在函数配置里指定了serviceAccount: SERVICE_ACCOUNT_EMAIL,要确保:

  • 这个服务账号在GCP项目里被授予roles/iam.serviceAccountUser角色(允许Cloud Function扮演该账号)
  • 去Cloud Function详情页,查看「运行时服务账号」是否和你指定的完全一致,有没有拼写错误
  • 用Firebase CLI部署的话,检查firebase.json里的函数配置有没有覆盖这个设置

3. 给代码加日志,确认Auth用的是正确账号

在initializeAuth里加一行日志,避免Auth自动取了默认服务账号:

const initializeAuth = async () => {
  try {
    const auth = new google.auth.GoogleAuth({
      scopes: ["https://www.googleapis.com/auth/androidpublisher"],
    });
    authClient = auth;
    // 新增:打印当前使用的服务账号
    const client = await auth.getClient();
    console.log("Auth initialized with service account:", client.email);
    androidpublisher = google.androidpublisher({
      version: "v3",
      auth: authClient,
    });
    console.log("Auth client and Android Publisher initialized successfully.");
  } catch (error) {
    console.error("Failed to initialize auth client:", error);
    throw new Error("Could not initialize Google Auth.");
  }
};

部署后看函数日志,确认打印的邮箱和你指定的完全一致。

4. 确认Play Developer API已启用

去GCP控制台的「API和服务」->「库」,搜索「Google Play Android Developer API」,确认它已被启用(默认可能未开启)。

5. 本地测试排除环境问题

如果以上都做了还是不行,本地用服务账号密钥测试API调用:

  1. 从GCP IAM下载服务账号的JSON密钥文件
  2. 本地设置环境变量:export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/key.json"
  3. 写个简单脚本调用purchases.subscriptions.get,用有效购买令牌测试
    • 本地成功说明权限没问题,问题出在Cloud Function运行时配置
    • 本地也失败则肯定是服务账号没在Play后台授权

最后一个小细节

刚创建的服务账号,权限可能需要10-15分钟同步到Play API,配置完不要立刻测试,等一会儿再试。

内容来源于stack exchange

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.07 09:52:59