同步處理使用者授權 (伺服器端整合)

發布商主要使用伺服器端整合功能來管理讀者和其授權。發布商主要會使用 UpdateReaderEntitlements 更新 Google 的 PPID 產品 ID 授權記錄。

Google Cloud 設定

在 Google Cloud 中設定訂閱連結功能包含兩個主要元件:

  1. 為特定專案啟用 API
  2. 建立用於存取 API 的服務帳戶

啟用 Subscription Linking API

如要使用服務帳戶及管理讀者的授權,Google Cloud 專案必須同時啟用訂閱連結 API,並正確設定 OAuth 服務帳戶。如要為專案啟用「Subscription Linking API」,請依序前往選單 ->「API 和服務」->「程式庫」,然後搜尋 Subscription Linking,或直接前往該頁面:


https://console.cloud.google.com/apis/library?project=gcp_project_id

api

圖 1. 前往 API 程式庫,並為 Google Cloud 專案啟用 API。

建立服務帳戶

服務帳戶可讓應用程式存取 Subscription Linking API。

  1. 在專案的控制台中建立服務帳戶
  2. 為服務帳戶建立憑證,並將 credentials.json 檔案儲存在應用程式可存取的安全位置。
  3. 將 IAM 角色「訂閱連結管理員」授予您建立的服務帳戶。如要精細控管服務帳戶的功能,您可以根據下表指派適當的角色。
功能 / 角色 訂閱連結管理員 訂閱連結檢視者 訂閱連結授權檢視者
取得讀者授權
吸引讀者
更新讀者授權
刪除讀者

搭配使用服務帳戶與 Subscription Linking API

如要使用服務帳戶驗證對 Subscription Linking API 的呼叫,請使用 googleapis 用戶端程式庫用戶端程式庫 (會自動處理 access_token 要求),或直接使用 REST API 簽署要求。如果使用 REST API,您必須先取得 access_token (透過 Google 驗證程式庫或使用服務帳戶 JWT),然後將其納入 Authorization 標頭

以下的用戶端程式庫REST API 範例都提供如何呼叫 getReader()getReaderEntitlements()updateReaderEntitlements()deleteReader() 的程式碼範例。

用戶端程式庫

本節說明如何在 Node.js 中使用 googleapis 用戶端程式庫

要求範例

針對 Auth.GoogleAuth 建構函式中的 keyFile 欄位,請設定服務帳戶金鑰的路徑。如果您因機構政策而無法匯出服務帳戶金鑰,可以使用帳戶預設憑證 (ADC) 方法。如果您採用 ADC 方法,則不需要提供 keyFile 欄位,因為 ADC 會自行搜尋憑證

import {readerrevenuesubscriptionlinking_v1, Auth} from 'googleapis';
const subscriptionLinking = readerrevenuesubscriptionlinking_v1.Readerrevenuesubscriptionlinking;

class SubscriptionLinking {
  constructor() {
    this.auth = new Auth.GoogleAuth({
      keyFile: process.env.KEY_FILE,
      scopes: [
        'https://www.googleapis.com/auth/readerrevenue.subscriptionlinking.manage'
      ],
    })
  }

  init() {
    return new subscriptionLinking(
        {version: 'v1', auth: this.auth})
  }
}

const subscriptionLinkingApi = new SubscriptionLinking();
const client = subscriptionLinkingApi.init();

/**
 * Retrieves details for a specific reader associated with the publication.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader.
 * @return {Promise<object>} A promise that resolves with the reader's details
 *  from the API.
 */
async function getReader(ppid) {
  const publicationId = process.env.PUBLICATION_ID;
  return await client.publications.readers.get({
    name: `publications/${publicationId}/readers/${ppid}`,
  });
};

/**
 * Updates the entitlements for a specific reader.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader whose
 *  entitlements are being updated.
 * @return {Promise<object>} A promise that resolves with the result of the
 *  updated entitlements object.
 */
async function updateReaderEntitlements(ppid) {
  const publicationId = process.env.PUBLICATION_ID;
  const requestBody = {
    /*
    Refer to
    https://developers.google.com/news/subscribe/subscription-linking/appendix/glossary#entitlements_object
    */
    entitlements : [{
      product_id: `${publicationId}:basic`,
      subscription_token: 'abc1234',
      detail: 'This is our basic plan',
      expire_time: '2025-10-21T03:05:08.200564Z'
    }]
  };
  return await client.publications.readers.updateEntitlements({
    name: `publications/${publicationId}/readers/${ppid}/entitlements`,
    requestBody
  });
};

/**
 * Retrieves the current entitlements for a specific reader.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader.
 * @return {Promise<object>} A promise that resolves with the reader's entitlements object.
 */
async function getReaderEntitlements(ppid) {
  const publicationId = process.env.PUBLICATION_ID;
  return await client.publications.readers.getEntitlements({
    name: `publications/${publicationId}/readers/${ppid}/entitlements`
  });
};

/**
 * Deletes a specific Subscription Linkikng reader record associated with the publication.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader to be deleted.
 * @param {boolean=} forceDelete - If true, delete the user even if their
 *  entitelements are not empty
 * @return {Promise<object>} A promise that resolves upon successful deletion
 *  with an empty object ({})
 */
async function deleteReader(ppid, forceDelete = false) {
  const publicationId = process.env.PUBLICATION_ID;
  return await client.publications.readers.delete({
    name: `publications/${publicationId}/readers/${ppid}`
    force: forceDelete
  });
};

REST API

如果您想呼叫 REST API 端點,可以使用任一方法取得 accessToken,並將其設為 Authorization 標頭。

1. 使用 GoogleAuth 程式庫

針對 credentials 金鑰,您可以使用服務帳戶金鑰帳戶預設憑證 (ADC)。如果您採用 ADC 方法,則無須提供 credentials 欄位,因為 ADC 會自行搜尋憑證

import { GoogleAuth } from 'google-auth-library';
import credentialJson from 'path_to_your_json_file' with { type: 'json' };

const auth = new GoogleAuth({
    credentials: credential_json,
    scopes: [
      'https://www.googleapis.com/auth/readerrevenue.subscriptionlinking.manage'
    ]
});

async function getAccessToken() {
    const accessToken = await auth.getAccessToken();
    return accessToken;
}

2. 使用服務帳戶 JWT 產生 access_token

import fetch from 'node-fetch';
import jwt from 'jsonwebtoken';

function getSignedJwt() {
  /*
    Either store the service account credentials string in an environmental variable
    Or implement logic to fetch it.
  */
  const key_file = process.env.CREDENTIALS_STRING

  const issueDate = new Date();
  const expireMinutes = 60;
  const offsetInSeconds = issueDate.getTimezoneOffset() * 60000;
  const expireDate = new Date(issueDate.getTime() + (expireMinutes * 60000));
  const iat = Math.floor((issueDate.getTime() + offsetInSeconds) / 1000);
  const exp = Math.floor((expireDate.getTime() + offsetInSeconds) / 1000);

  const token = {
    iss: key_file.client_email,
    iat,
    exp,
    aud: 'https://oauth2.googleapis.com/token',
    scope:'https://www.googleapis.com/auth/readerrevenue.subscriptionlinking.manage',
  }
  return jwt.sign(token, key_file.private_key, {
    algorithm: 'RS256',
    keyid: key_file.private_key_id,
  })
}

async function getAccessToken(signedJwt) {
  let body = new URLSearchParams();
  body.set('grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer');
  body.set('assertion', signedJwt);
  const response = await fetch('https://oauth2.googleapis.com/token', {
    method: 'POST',
    headers: {'Content-Type': 'application/x-www-form-urlencoded'},
    body
  })
  const accessResponse = await response.json();
  return accessResponse.access_token;
}

使用 Google Auth 程式庫呼叫 REST API 的程式碼範例

import { GoogleAuth } from 'google-auth-library';
import fetch from 'node-fetch'
import credentialJson from 'path_to_your_json_file' with { type: 'json' };

const BASE_SUBSCRIPTION_LINKING_API_URL='https://readerrevenuesubscriptionlinking.googleapis.com/v1';
const publicationId = process.env.PUBLICATION_ID

const auth = new GoogleAuth({
    credentials: credentialJson,
    scopes: [
      'https://www.googleapis.com/auth/readerrevenue.subscriptionlinking.manage'
    ]
});

async function getAccessToken() {
    const accessToken = await auth.getAccessToken();
    return accessToken;
}

/**
 * Retrieves details for a specific reader associated with the publication.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader.
 * @return {object} reader json for the given ppid
 */
async function getReader(ppid) {
  const endpoint = `${BASE_SUBSCRIPTION_LINKING_API_URL}/publications/${publicationId}/readers/${ppid}`;
  const accessToken = await getAccessToken();
  const response = await fetch(endpoint, {
     method: 'GET',
     headers: {
       Authorization: `Bearer ${accessToken}`,
     },
   });
  const reader = await response.json();
  return reader;
}

/**
 * Updates the entitlements for a specific reader.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader.
 * @return {object} the updated entitlements object in json.
 */
async function updateReaderEntitlements(ppid) {
  const endpoint = `${BASE_SUBSCRIPTION_LINKING_API_URL}/publications/${publicationId}/readers/${ppid}/entitlements`;
  const requestBody = {
    /*
    Refer to
    https://developers.google.com/news/subscribe/subscription-linking/appendix/glossary#entitlements_object
    */
    entitlements : [{
      product_id: `${publicationId}:basic`,
      subscription_token: 'abc1234',
      detail: 'This is our basic plan',
      expire_time: '2025-10-21T03:05:08.200564Z'
    }]
  };
  const response = await fetch(endpoint, {
     method: 'PATCH',
     headers: {
       Authorization: `Bearer ${accessToken}`,
       'Content-Type': 'application/json',
     },
     body: JSON.stringify(requestBody)
   })
  const updatedEntitlements = await response.json();
  return updatedEntitlements;
}

/**
 * Retrieves the current entitlements for a specific reader.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader.
 * @return {object} the reader's entitlements object in json.
 */
async function getReaderEntitlements(ppid) {
  const endpoint = `${BASE_SUBSCRIPTION_LINKING_API_URL}/publications/${publicationId}/readers/${ppid}/entitlements`;
  const accessToken = await getAccessToken();
  const response = await fetch(endpoint, {
     method: 'GET',
     headers: {
       Authorization: `Bearer ${accessToken}`,
     },
   });
  const entitlements = await response.json();
  return entitlements;
}

/**
 * Deletes a specific Subscription Linkikng reader record associated with the publication.
 * @async
 * @param {string} ppid - The Publisher Provided ID (ppid) for the reader.
 * @param {boolean=} forceDelete - If true, delete the user even if their
 *  entitelements are not empty
 * @return {object} returns an empty object ({}) if the delete operation is successful
 */
async function deleteReader(ppid, forceDelete = false) {
  const endpoint = `${BASE_SUBSCRIPTION_LINKING_API_URL}/publications/${publicationId}/readers/${ppid}?force=${forceDelete}`;
  const response = await fetch(endpoint, {
     method: 'DELETE',
     headers: {
       Authorization: `Bearer ${accessToken}`,
     }
   });
  const result = await response.json();
  return result;
}