🔑

Google PlatformのAPIキーにリクエスト制限をつけて、APIが使えなくなったあなたへ

2024/11/03に公開

先日、Expoでアプリを開発しているときに、Google Cloudで発行したAPIキーに制限をかけたら、APIが呼び出せなくなる現象が発生したため記事にしてみました。

この記事を読むとわかること・解決すること

  • ※ Expo以外にも、ネイティブアプリを開発していてGoogle公式のライブラリが使えない方にも応用が効くと思います。
  • ExpoでGoogle Places API使いたいけど、いいライブラリが見つからない
  • APIキーの呼び出し元に制限をつけると動かない!
  • HTTPクライアントでリクエストを送信したい

実験のために作成したリポジトリもあるので、よろしければ参考にしてみてください 👋
https://github.com/zackerms/expo-playground-google-places-api

公式ライブラリじゃないと、うまく動かない!

Google Cloudで発行されるAPIには呼び出し元に以下のような制限をかけることができます。

  • Webサイト
  • Android(パッケージ名やフィンガープリント)
  • iOS(パッケージ名)

しかし、呼び出し元についてAndroidやiOSの制約をかけた場合、公式ライブラリを利用しないとリクエストが拒否されるという現象が発生することがあります。

サードパーティライブラリが微妙!

公式ライブラリを利用しなくても、そのライブラリをラップしたサードパーティのライブラリを利用することで間接的に公式ライブラリを利用することも可能です。 特に、Expo(React Native)の開発では公式ライブラリが存在しないため、そのようなライブラリを使うことがあります。

私はGoogle Places APIを利用していたのですが、そのサードパーティライブラリには以下のようなものがありました。

ライブラリ メリット 懸念点
react-native-google-places-autocomplete React Componentだけでなく、純粋なAPIクライアントを提供してくれている。 リクエストやレスポンスの内容が公式のものからだいぶ削がれているため、必要なデータを取得できないことがある。
expo-google-places わりと細かいリクエストの設定ができそう 開発途中
react-native-google-places-sdk わりと細かいリクエストの設定ができそう 開発途中

このように開発途中のものや、必要なデータが得られないものしかなく、サードパーティのライブラリを利用することを断念しました。

HTTPクライアントでリクエストを送信したい!

サードパーティのライブラリまで使えないとなると、HTTPリクエストを直書きする必要があります。 しかし、単純に公式ライブライに書かれているようにAPIキーを指定してリクエストを送信するだけでは拒否されてしまいます。 その理由はAPIの呼び出しもとが、登録されたAndroid/iOSアプリであることを証明できないからです。

公式ドキュメントによると以下のようなヘッダを指定すると、呼び出し元を伝えられるようです。

  • Android
    • X-Android-Package:Androidアプリのパッケージ名
    • X-Android-Cert: Androidアプリのフィンガープリント
  • iOS
    • X-Ios-Bundle-Identifier : iOSアプリのパッケージ名

それぞれの値は、APIの呼び出し制限で登録した値と一致している必要があります。

Androidのフィンガープリントを取得する

フィンガープリントを取得するためには、まず android ディレクトリを生成する必要があります

# npm
npm run expo prebuild --platform android

# yarn
yarn expo prebuild --platform android

次に、デバッグ用なfingerprintを取得します。

パスワードを聞かれますが、何も入力せずEnterを押してください。

keytool -list -v -keystore ./android/app/debug.keystore

するとたくさんの情報のなかに以下のようなものが出力されます。

証明書のフィンガプリント:
         SHA1: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00
         SHA256: 00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00

SHA1で表記された部分をリクエストヘッダに登録します。

パッケージ名を取得する

パッケージ名も環境変数で指定することもできますが、Expoのライブラリから取得することができます。(公式ドキュメント

npx expo install expo-application
import { applicationId } from 'expo-application';

const packageName = applicationId;

リクエストヘッダにパッケージ名やFingerprintを含める[Android]

以下をベースとなるリクエストとして利用します。

import axios from "axios";

const response = await axios.get("https://maps.googleapis.com/maps/api/place/details/json", {
  params: {
    place_id: placeId,
    language: "ja",
    key: API_KEY,
  },
});

カンタンに説明すると、このコードを実行すると以下のようなURLにGETリクエストが飛びます。

https://maps.googleapis.com/maps/api/place/details/json?place_id=placeId&language="ja"&key=<api_key>

リクエストヘッダに以下のようにパッケージ名やフィンガープリントを登録することで、APIの呼び出し元の制限をクリアすることができます。

import axios from "axios";
import { applicationId } from "expo-application";

const response = await axios.get("https://maps.googleapis.com/maps/api/place/details/json", {
  params: {
    place_id: placeId,
    language: "ja",
    key: API_KEY,
  },
  header: {
     "X-Android-Package": applicationId,
     "X-Android-Cert": "00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00:00".replaceAll(":", ""),
  }
});

ここが落とし穴なのですが、フィンガープリントをヘッダーに入力するときにはハッシュ内のを削除する必要があります。

リクエストヘッダにパッケージ名を含める[iOS]

iOS版も同様にパッケージ名を含めます。

import axios from "axios";
import { applicationId } from "expo-application";

const response = await axios.get("https://maps.googleapis.com/maps/api/place/details/json", {
  params: {
    place_id: placeId,
    language: "ja",
    key: API_KEY,
  },
  header: {
      "X-Ios-Bundle-Identifier": applicationId,
  }
});

プラットフォームに応じて自動的にヘッダの内容を変える

import { Platform } from "react-native";
import axios from "axios";
import { applicationId } from "expo-application";

function getRequestHeader() {
  if (Platform.OS === "android") {
      return {
          "X-Android-Package": applicationId,
          // https://stackoverflow.com/questions/61518968/using-google-api-key-with-restriction-in-android
          "X-Android-Cert":
              AppEnv.EXPO_ANDROID_SHA_1_FINGERPRINT.replaceAll(":", ""),
      };
  }
  
  if (Platform.OS === "ios") {
      return {
          "X-Ios-Bundle-Identifier": applicationId,
      };
  }

  return {};
}

const response = await axios.get("https://maps.googleapis.com/maps/api/place/details/json", {
  params: {
    place_id: placeId,
    language: "ja",
    key: API_KEY,
  },
  header: getRequestHeader() 
});

さいごに

スキマ時間で対応してたということもありますが、この対応に1週間くらいの時間を溶かしてしまいました。 自分が制作している大きなプロジェクトの中ではなく、新しく小さなプロジェクトを作ることで解決の糸口を見出すことができました。 そういうことってありますよね。

この記事が誰かの役に立てると嬉しいです!

[宣伝] komichi というアプリを作っています!

「どこ行こうかなぁ〜」と考えてたら休日が終わることってありませんか?
このアプリを使うと、現在地や好きな場所から、自動的に付近の場所を検索してお出かけプランを提案してくれます!
見知った場所でも意外としらないスポットが出てきて面白いですよ!
https://komichi.app

先日、Android版をリリースいたしましたので、Androidユーザの方はぜひインストールお願いします!
https://play.google.com/store/apps/details?id=app.komichi&hl=ja

Discussion