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

火山引擎 最新活动