この記事は「もくもく会ブログリレー」 8 日目 の記事です。
APIキーの取り扱いについて
ウェブ開発を行っていると、APIを呼び出す場面は多いと思います。その際、「APIキーを安全に秘匿する方法」は一つ重要なテーマとなります。
例えば、「外部APIを呼び出す必要があるが、APIキーをクライアントサイドに埋め込むとセキュリティ上のリスクが高い」ことは気をつけないといけません。クライアントサイドのコードはユーザーに容易にアクセスされるため、APIキーが漏洩するリスクがあります。
こうした問題を解決するために役立つのが、Next.jsのRoute Handlersです。Route Handlersを使うことで、APIキーをサーバサイドで安全に管理しつつ、クライアントサイドから安全にAPIを呼び出すことが可能になります。
実際どう危険なのか
Gemini APIキーを使用して具体例を見てみます。次のように定義されたsubmitText
関数をクライアントサイドで呼び出すとします。この関数は、ユーザーから入力されたテキストをGemini APIに送信し、その応答を取得するためのものです。
export const submitText = async (text: string) => { const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${process.env.NEXT_PUBLIC_GEMINI_API_KEY}`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ contents: [{ parts: [{ text }] }] }), }); const data = await response.json(); return data; };
こちらのsubmitText
関数を以下のようにクライアントサイドから叩きます。
"use client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { submitText } from "@/data/api"; import { useState } from "react"; import ReactMarkdown from "react-markdown"; export default function Gemini() { const [text, setText] = useState(""); const [responseMessage, setResponseMessage] = useState(""); const [loading, setLoading] = useState(false); const onSubmit = async () => { setLoading(true); const data = await submitText(text); const message = data.candidates[0].content.parts[0].text; setResponseMessage(message); setLoading(false); }; return ( <div> <h1>Gemini Test</h1> <div> setText(e.target.value)} placeholder="メッセージを入力" /> <Button type="submit"> 送信 </Button> </div> <p>Gemini の返答</p> {loading ? ( <p>問い合わせ中...</p> ) : ( {responseMessage} )} </div> ); }
リクエストしたAPIがネットワークタブからkey=~~~~~
という形でAPIキーが見えてしまうことが下の画像からわかります。APIキーが漏洩すると、不正アクセスにつながる可能性があります。
このように定義されたsubmitText
関数は、クライアントサイドから直接外部APIを呼び出すため、APIキーがネットワークリクエストに含まれてしまいます。これにより、APIキーがネットワークタブに表示され、セキュリティ上のリスクが高まります。
このセキュリティリスクを回避するための策の一つとして、Route Handlersを使います。
Route Handlersとは
Next.jsのRoute Handlersは、APIエンドポイントを簡単に作成できる機能です。これにより、サーバサイドでの処理を行うためのエンドポイントを作成し、クライアントサイドからのHTTPリクエストを処理することができます。Route Handlersを使用することで、APIキーをサーバサイドで管理し、クライアントサイドに露出することなくAPIを呼び出すことが可能になります。
どこに書けばいい?
Route Handlersの配置場所は、App Routerのルーティング方法と似ています。例えば、App Routerでは/products/listというURLに対応するページを作成する場合、次のようなディレクトリ構造でファイルを配置します。
app/ └── products/ └── list/ └── page.tsx
同様に、Route Handlersを使ってエンドポイントを作成する場合は、次のようにディレクトリを構成します。例えば、/api/submitというエンドポイントを作成するには、以下のようにファイルを配置します。
app/ └── api/ └── submit/ └── route.ts
Route Handlersを作成する
Route Handlersでは、処理したいHTTPリクエストの種類に応じて関数を定義します。例えば、今回はPOSTリクエストを処理するために、関数名をPOSTとなります。
まず、エンドポイントを設定します。app/api/submit/route.ts
ファイルを作成し、POSTリクエストを処理する関数を定義します。
import { type NextRequest, NextResponse } from "next/server"; export async function POST(request: NextRequest) { const { text } = await request.json(); const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash-latest:generateContent?key=${process.env.GEMINI_API_KEY}`, { method: "POST", body: JSON.stringify({ contents: [{ parts: [{ text }] }] }), }, ); const data = await res.json(); return NextResponse.json(data); }
クライアントサイドからRoute Handlersにリクエストを送る
APIエンドポイントを作成したのでこれをクライアントサイドから呼び出しましょう。上記のエンドポイントをクライアントサイドから呼び出すためのコードを作成します。
先ほどのsubmitTextメソッドを以下のように変更します。このメソッドは、クライアントサイドから新しいエンドポイント/api/submit
にリクエストを送信するために使用されます。
export const submitText = async (text: string) => { const response = await fetch("/api/submit", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ text }), }); const data = await response.json(); return data; };
これにより、クライアントサイドからのリクエストがサーバサイドのRoute Handlerを通じて処理されるようになります。APIキーはサーバサイドで管理されるため、クライアントサイドに露出することはありません。
まとめ
Route Handlersを用いたAPIキーの秘匿化についてでした。
APIキー以外にも秘匿化すべき、見えてはいけない情報を扱うときは、クライアントサイドの実行では情報の取得が容易であることを意識して、適切に管理することを心掛けましょう!
明日の記事は、Ayumi Sashitani さんの「 Dart3.4の新機能Macrosで静的メタプログラミングを試してみた 」です!