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调用:
- 从GCP IAM下载服务账号的JSON密钥文件
- 本地设置环境变量:
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/your/key.json" - 写个简单脚本调用
purchases.subscriptions.get,用有效购买令牌测试- 本地成功说明权限没问题,问题出在Cloud Function运行时配置
- 本地也失败则肯定是服务账号没在Play后台授权
最后一个小细节
刚创建的服务账号,权限可能需要10-15分钟同步到Play API,配置完不要立刻测试,等一会儿再试。
内容来源于stack exchange




