🌩️

API キーを隠すための Proxy Server を Vercel Serveless Function で実装する

2022/06/17に公開

TL;DR

サンプルリポジトリ

https://github.com/ryokryok/react-with-serverless

クライアントサイドから外部 API キーを使った通信、そのまま実装していませんか?

例えば React 等でクライアントサイド側から外部の API を叩いて構築するアプリがあったとします。
認証が必要な API の場合、API キーを発行して、それをクエリパラメーター、またはヘッダーに付与する形で API のエンドポイントにアクセスします。

ローカルでの開発段階では特に問題がありませんが、作成したサイトを外部に公開する場合は問題があります。
通信経路がそのままの場合、Google Chrome のコンソールでどの URL に、どのようなヘッダーを付与してリクエストしたのかが分かってしまうためです。

下記は筆者が過去に作成したアプリですが、何の対策もしていないため API キーが簡単に分かってしまいます。

expose-api-key

API キーを盗まれるとどうなるのか

それでは API キーが盗まれるとどうなるのかです。

  • API キーが使用上限いっぱいまで乱用され、サイトの機能を停止させられてしまう
    • 従量課金制の場合は課金されてしまう可能性がある
  • URL からどの API サービスを使っているのか特定して、他のエンドポイントを叩かれてしまう
    • GET 系ならまだしも、POST 系のエンドポイントの場合は勝手にデータを書き換えられる可能性がある

基本無料で利用できる API でも大変ですが、有料の API の場合はもっと大変です。

なので、API キーを通信経路から隠す必要があります。

API キーを隠す対策

方針:サーバーサイドのアプリで外部 API を叩く

API の通信経路を隠す対策はクライアントサイドだけでは難しいです。通信経路や API キーを環境変数で渡したとしても、ビルドしたものにはその情報が埋め込まれているため、公開時に Chrome のコンソールで見えてしまいます。
そのため、方針として代わりに外部 API を叩くサーバーサイドの処理が必要になります。

Next.js の API Routes を利用する

例えば Next.js 等のフレームワークを使っている場合は、API Routes に通信してもらうという方法があります。
この場合、API Routes はサーバーサイドの実行になるので API を秘匿することが可能です。

https://nextjs.org/docs/api-routes/introduction

ただし、素朴な React で構築されたような SPA アプリケーションの場合はこの方法は使えません。

Serverless Function で外部 API を叩く

AWS Lambda 等の Serverless Function 系の場合、基本的に実行した時のみ料金が発生します。
また、Vercel Serverless Functions や Netlify Functions など、無料で利用できるものもあります。

そのため、常時稼働するようなサーバーサイドアプリをデプロイするよりは比較的コストを下げる、または無料でできます。

関数単位で記述できるため、今回のような外部 API の API キーを隠す用途の場合でもあまりコードが多くならず記述できます。
API キーの秘匿以外にデータ加工やヘッダーの変更をすることも可能です。
今回は無料で作成できる Vercel Serverless Function を使います。

Vite で作成した React アプリに Serverless Function を追加する

前置きが長くなりましたが作っていきます。

今回はクライアントサイドのアプリと同一のリポジトリ内に Serverless Functions のコードが存在する想定で作成します。
また、サンプルの外部 API として RESAS-API を使っていきます。
実際に動かす場合は API キーを発行してください。

https://opendata.resas-portal.go.jp/docs/api/v1/prefectures.html

# Serverless Functions のローカルでの開発のために Vercel CLI が必要です
$ npm i -g vercel
$ yarn create vite react-with-serverless --template react-ts
$ cd react-with-serverless
$ yarn
# Serverless Functions 内で API を叩くため。執筆時点で 3.x 系だとエラーがあるため 2.x を指定
$ yarn add node-fetch@2.x
$ yarn add -D @types/node-fetch
# Serverless Functions の型
$ yarn add -D @vercel/node
$ mkdir api && touch api/prefectures.ts
# 環境変数記述用、 .gitignore に追加してコミットしないようにすること
$ touch .env
// .env
API_KEY=YOUR_API_KEY
// api/prefectures.ts
import type { VercelRequest, VercelResponse } from '@vercel/node'
import fetch from 'node-fetch'

type ApiResponse = {
  statusCode?: string
  message: string | null
  description?: string
  result?: Prefectures[]
}

type Prefectures = {
  prefCode: number
  prefName: string
}

const API_URL = 'https://opendata.resas-portal.go.jp/api/v1/'

const API_KEY = process.env.API_KEY ?? ''

async function fetchPrefectures(): Promise<ApiResponse> {
  const response = await fetch(`${API_URL}prefectures`, {
    headers: {
      'X-API-KEY': API_KEY,
    },
  })
  const data = await response.json()
  return data
}

export default async function (req: VercelRequest, res: VercelResponse) {
  const result = await fetchPrefectures()
  res.json(result)
}

関数の作成が完了したら書きを実行して開発サーバーを開始します。
質問が聞かれますが、Enter 連打で OK です。

$ vercel dev

成功すれば http://localhost:3000/api/prefectures にアクセスすると JSON 形式のレスポンスが帰ってくることが確認できます。

クライアントサイドで呼び出す

クライアントサイド側から呼び出します。 Serverless Function と同一オリジン上に存在するので相対パスで記述できます。

async function fetchApi(): Promise<ApiResponse> {
  const res = await fetch('/api/prefectures')
  const data = await res.json()
  return data
}

後は React コンポーネント内で呼び出します。その辺りの記述はリポジトリを見てください。

https://github.com/ryokryok/react-with-serverless/blob/main/src/App.tsx

Cache-Control を指定する

この状態でも動作しますが、API によってはあまり頻繁に更新されないものもあります。
例えば今回の API では都道府県を取得していますが、頻繁に変わるデータではありません。
こういう場合はレスポンスヘッダーに Cache-Control を設定し、2 回目のアクセスからはキャッシュを利用するようにブラウザに伝えます。

// api/prefectures.ts
+  // cache for 1 days
+  res.setHeader('Cache-Control', 's-maxage=86400 immutable')

Status Code : 304 を返していることが確認できます。

response_status_304

まとめ

API キーを保護するための Proxy Server は簡単に作れるので気軽に作りましょう。
今回は未検証ですが、Cloudflare Workers のようなエッジサーバーで API を叩くのもできそうですね。
https://developers.cloudflare.com/workers/examples/bulk-origin-proxy/

Discussion