从 Firebase 函数调用 Google Play Developer API

Call Google Play Developer API from Firebase Functions

我正在尝试为用户的应用内购买和订阅开发服务器端验证 recommended,我想为此使用 Firebase Functions。基本上它必须是一个接收购买令牌的 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 APIs),这个事实非常令人失望。我设法在这里和那里找到了一些信息,但是对 Google APIs 的经验太少(其中大部分只需要使用 api 键),我需要帮助把它放在一起。

这是我到目前为止的发现:

  1. 我有一个 GCP 项目链接到 Play 商店并启用了 API。但出于某种原因,尝试在 APIs Explorer 中测试它会导致错误 "The project id used to call the Google Play Developer API has not been linked in the Google Play Developer Console"。
  2. 我创建了一个服务帐户并导出了一个 JSON 密钥,其中包含生成 JWT 的密钥。
  3. 我还在 Play 管理中心为该服务帐户设置了读取权限。
  4. 我找到了 Google APIs 的 Node.JS client library,它处于 alpha 阶段并且文档非常稀疏(例如,没有关于如何使用 JWT 进行身份验证的明显文档,也没有有关如何调用 android 发布者 API 的示例)。目前我正在努力解决这个问题。不幸的是,我不太喜欢阅读 JS 库代码,尤其是当编辑器不提供跳转到突出显示的函数源代码的可能性时。

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

我自己想出来了。我还放弃了重量级客户端库,只是手动编写了那几个请求。

备注:

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

在您获得 JSON 文件以及链接到 Play 商店的服务帐户的私钥后,调用 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 是我遵循的文档。

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

为了支持缩放并防止 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;
}

这就是我们做那件事的方式。