【问题标题】:Call Google Play Developer API from Firebase Functions从 Firebase 函数调用 Google Play Developer API
【发布时间】:2018-09-24 22:40:54
【问题描述】:

我正在尝试以recommended 为我的用户的应用内购买和订阅开发服务器端验证,我想为此使用 Firebase 函数。基本上它必须是一个 HTTP 触发函数,它接收购买令牌,调用 Play Developer API 来验证购买,然后对结果进行处理。

但是,调用许多 Google API(包括Play Developer API)需要重要的授权。以下是我对所需设置的理解:

  1. 必须有一个启用了 Google Play Developer API v2 的 GCP 项目。
  2. 它应该是一个单独的项目,因为在 Google Play 控制台中只能有一个链接到 Play 商店。
  3. 我的 Firebase Functions 项目必须以某种方式向该其他项目进行身份验证。我认为使用服务帐户最适合这种服务器到服务器的场景。
  4. 最后,我的 Firebase Functions 代码必须以某种方式获取身份验证令牌(希望是 JWT?),最后进行 API 调用以获取订阅状态。

问题是绝对不存在人类可读的文档或指导。鉴于 Firebase 中的入口流量包含在免费计划中(所以我认为他们鼓励使用 Firebase Functions 中的 Google API),这一事实非常令人失望。我已经设法在这里和那里找到了一些信息,但是我对 Google API 的经验太少(其中大部分只需要使用一个 api 密钥),我需要帮助将它们组合在一起。

到目前为止,这是我想出的:

  1. 我获得了一个链接到 Play 商店并启用了 API 的 GCP 项目。但由于某种原因,尝试在 APIs Explorer 中对其进行测试会导致错误“用于调用 Google Play Developer API 的项目 ID 尚未在 Google Play Developer Console 中链接”。
  2. 我创建了一个服务帐户并导出了一个 JSON 密钥,其中包含生成 JWT 的密钥。
  3. 我还在 Play 管理中心中为该服务帐号设置了读取权限。
  4. 我找到了一个用于 Google API 的 Node.JS client library,它是 alpha 版,文档非常稀少(例如,没有关于如何使用 JWT 进行身份验证的明显文档,也没有关于如何调用 android 发布者 API 的示例)。目前我正在为此苦苦挣扎。不幸的是,我不习惯阅读 JS 库代码,尤其是当编辑器无法跳转到突出显示的函数的源代码时。

我很惊讶这没有被询问或记录,因为从 Firebase Functions 验证应用内购买似乎是一项常见任务。之前有没有人成功完成过,或者 Firebase 团队可能会介入回答?

【问题讨论】:

  • 嘿,我想问你一些关于这个的问题。您是否创建了单独的服务帐户,或者您可以使用 firebase json 作为公钥和私钥?
  • @ABDevelopers Play Store API 只能从 one GCP 项目(Firebase 项目也由 GCP 项目支持)访问,因此创建一个帐户是有意义的,但一个完全独立的 GCP 项目 仅用于访问 API。实际上这最近已经被简化了:您只需按照 Google Play 控制台 -> 设置 -> API 访问中的说明进行操作。当然,您可以改为链接现有的 Firebase 项目,但当您最终想从另一个应用程序访问 API 时,您可能需要重新配置。
  • 我非常感谢您为问答所做的努力,这对我很有帮助。您对 firebase cron 作业有任何想法吗?
  • @ABDevelopers 据我所知,他们没有这些。您可以按照this Firebase blog entry 中的建议在 Google Cloud 中设置 App Engine 实例,或者使用一些外部服务,例如cron-job.org,在所需时间发出 HTTP 请求,然后您可以在 Functions 中处理这些请求。这是一个单独的主题。

标签: firebase google-api google-cloud-functions google-api-nodejs-client google-play-developer-api


【解决方案1】:

我自己想通了。我还放弃了重量级的客户端库,只手动编写了这几个请求。

注意事项:

  • 这同样适用于任何 Node.js 服务器环境。您仍然需要单独服务帐号的密钥文件来创建 JWT 和调用 API 的两个步骤,Firebase 也不例外。
  • 这同样适用于其他需要身份验证的 API — 不同之处仅在于 JWT 的 scope 字段。
  • a few APIs 不需要您将 JWT 交换为访问令牌 - 您可以铸造 JWT 并直接在 Authentication: Bearer 中提供它,而无需往返 OAuth 后端。

在您获得包含与 Play 商店相关联的服务帐户私钥的 JSON 文件后,调用 API 的代码如下所示(根据您的需要进行调整)。注意:我使用request-promise 作为http.request 的更好方式。

const functions = require('firebase-functions');
const jwt = require('jsonwebtoken');
const keyData = require('./key.json');         // Path to your JSON key file
const request = require('request-promise');

/** 
 * Exchanges the private key file for a temporary access token,
 * which is valid for 1 hour and can be reused for multiple requests
 */
function getAccessToken(keyData) {
  // Create a JSON Web Token for the Service Account linked to Play Store
  const token = jwt.sign(
    { scope: 'https://www.googleapis.com/auth/androidpublisher' },
    keyData.private_key,
    {
      algorithm: 'RS256',
      expiresIn: '1h',
      issuer: keyData.client_email,
      subject: keyData.client_email,
      audience: 'https://www.googleapis.com/oauth2/v4/token'
    }
  );

  // Make a request to Google APIs OAuth backend to exchange it for an access token
  // Returns a promise
  return request.post({
    uri: 'https://www.googleapis.com/oauth2/v4/token',
    form: {
      'grant_type': 'urn:ietf:params:oauth:grant-type:jwt-bearer',
      'assertion': token
    },
    transform: body => JSON.parse(body).access_token
  });
}

/**
 * Makes a GET request to given URL with the access token
 */
function makeApiRequest(url, accessToken) {
  return request.get({
    url: url,
    auth: {
      bearer: accessToken
    },
    transform: body => JSON.parse(body)
  });
}

// Our test function
exports.testApi = functions.https.onRequest((req, res) => {
  // TODO: process the request, extract parameters, authenticate the user etc

  // The API url to call - edit this
  const url = `https://www.googleapis.com/androidpublisher/v2/applications/${packageName}/purchases/subscriptions/${subscriptionId}/tokens/${token}`;

  getAccessToken(keyData)
    .then(token => {
      return makeApiRequest(url, token);
    })
    .then(response => {
      // TODO: process the response, e.g. validate the purchase, set access claims to the user etc.
      res.send(response);
      return;
    })
    .catch(err => {
      res.status(500).send(err);
    });
});

These 是我关注的文档。

【讨论】:

  • 嗨@actine,目前我收到“权限被拒绝”错误(当前用户没有足够的权限来执行请求的操作)。这是我的服务 account.json 或其他问题的问题吗?请告诉我你如何创建你的 service-account.json (角色等..)
  • @thomasbabuj 仔细检查该服务帐户是否是连接到 Google Play 管理中心的帐户,以及它是否具有必要的权限(例如,您可以授予财务角色进行结算)。您在设置 -> API 访问 -> 服务帐户(页面上的最后一个表格)-> 授予访问/查看权限上设置角色
  • 感谢您的回复。我能够通过在游戏控制台中更新服务帐户电子邮件 ID 的角色来解决我的问题。再次感谢。
  • Actine,这可能正是我想要的。哇,文档看起来很糟糕。卡了几天。我对您要导入的内容感到困惑,jwt 是什么......您会考虑制作一个所有代码都是一个文件的版本吗?另外,为什么我没有看到 export.xxxxx,因为这应该是 Functions - 这是你的 index.js 文件吗?
  • @JeffPadgett 是的,这是 index.js,exports.testApi 在那里。不是最好的结构化代码,但对我和说明答案来说已经足够了。 jwtrequest 变量是导入的 npm 模块。 JWT 基本上是一种身份验证方式(真的很巧妙)——阅读here。这一个文件中的所有代码(除了 npm 模块和您从 GCP 控制台下载的 key.json)。
【解决方案2】:

我想我找到了一种稍微快一点的方法……或者至少……更简单。

为了支持缩放并防止 index.ts 失去控制...我在索引文件中拥有所有函数和全局变量,但所有实际事件都由处理程序处理。更容易维护。

所以这是我的 index.ts(我的心脏类型安全):

//my imports so you know
import * as functions from 'firebase-functions';
import * as admin from "firebase-admin";
import { SubscriptionEventHandler } from "./subscription/subscription-event-handler";

// honestly not 100% sure this is necessary 
admin.initializeApp({
    credential: admin.credential.applicationDefault(),
    databaseURL: 'dburl'
});

const db = admin.database();

//reference to the class that actually does the logic things
const subscriptionEventHandler = new SubscriptionEventHandler(db);

//yay events!!!
export const onSubscriptionChange = functions.pubsub.topic('subscription_status_channel').onPublish((message, context) => {
    return subscriptionEventHandler.handle(message, context);
});
//aren't you happy this is succinct??? I am!

现在......表演!

// importing like World Market
import * as admin from "firebase-admin";
import {SubscriptionMessageEvent} from "./model/subscription-message-event";
import {androidpublisher_v3, google, oauth2_v2} from "googleapis";
import {UrlParser} from "../utils/url-parser";
import {AxiosResponse} from "axios";
import Schema$SubscriptionPurchase = androidpublisher_v3.Schema$SubscriptionPurchase;
import Androidpublisher = androidpublisher_v3.Androidpublisher;

// you have to get this from your service account... or you could guess
const key = {
    "type": "service_account",
    "project_id": "not going to tell you",
    "private_key_id": "really not going to tell you",
    "private_key": "okay... I'll tell you",
    "client_email": "doesn't matter",
    "client_id": "some number",
    "auth_uri": "https://accounts.google.com/o/oauth2/auth",
    "token_uri": "https://accounts.google.com/o/oauth2/token",
    "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
    "client_x509_cert_url": "another url"
};

//don't guess this...  this is right
const androidPublisherScope = "https://www.googleapis.com/auth/androidpublisher";

// the handler
export class SubscriptionEventHandler {
    private ref: admin.database.Reference;

    // so you don't need to do this... I just did to log the events in the db
    constructor(db: admin.database.Database) {
        this.ref = db.ref('/subscriptionEvents');
    }

    // where the magic happens
    public handle(message, context): any {
        const data = JSON.parse(Buffer.from(message.data, 'base64').toString()) as SubscriptionMessageEvent;

        // if subscriptionNotification is truthy then we're solid here
        if (message.json.subscriptionNotification) {
            // go get the the auth client but it's async... so wait
            return google.auth.getClient({
                scopes: androidPublisherScope,
                credentials: key
            }).then(auth => {
                //yay!  success!  Build android publisher!
                const androidPublisher = new Androidpublisher({
                    auth: auth
                });

                // get the subscription details
                androidPublisher.purchases.subscriptions.get({
                    packageName: data.packageName,
                    subscriptionId: data.subscriptionNotification.subscriptionId,
                    token: data.subscriptionNotification.purchaseToken
                }).then((response: AxiosResponse<Schema$SubscriptionPurchase>) => {
                    //promise fulfilled... grandma would be so happy
                    console.log("Successfully retrieved details: " + response.data.orderId);
                }).catch(err => console.error('Error during retrieval', err));
            });
        } else {
            console.log('Test event... logging test');
            return this.ref.child('/testSubscriptionEvents').push(data);
        }
    }
}

有几个模型类可以提供帮助:

export class SubscriptionMessageEvent {
    version: string;
    packageName: string;
    eventTimeMillis: number;
    subscriptionNotification: SubscriptionNotification;
    testNotification: TestNotification;
}

export class SubscriptionNotification {
    version: string;
    notificationType: number;
    purchaseToken: string;
    subscriptionId: string;
}

所以我们就是这样做的。

【讨论】:

  • 嗨。很好的答案,但我总是收到错误“用于调用 Google Play 开发人员 API 的项目 ID 尚未在 Google Play 开发人员控制台中链接。”。有什么解决办法吗?
  • 谢谢。您需要确保您的 Google Cloud Functions 服务帐户已在您的 Google Play 开发者帐户中注册。可以在这里找到答案:stackoverflow.com/questions/25481207/…
  • 是的,由于某种原因这不起作用,但是在我使用全新的 GCP 项目重新完成所有操作后,它起作用了。现在,我有一个合乎逻辑的问题 - 在响应中,我们没有获得有关购买订阅的用户的任何信息。我们如何从购买令牌中找到用户的信息?我很喜欢,发现有一个名为 developerPayload 的字段,但很快就会被弃用。
  • 您应该在设备上本地拥有令牌的副本。您可以将其保存在设备上,然后使用它来引用您的服务器拥有的副本。
猜你喜欢
  • 2017-11-04
  • 2014-05-08
  • 2021-07-13
  • 2015-04-30
  • 1970-01-01
  • 1970-01-01
  • 2019-08-05
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多