外部APIを使う静的サイトでAPIキーを隠す:PHPで「薄いプロキシAPI」を挟む構成のサンプル

なぜ必要か

 筆者は実務で「chatGPTやgeminiのAPIを使う前提の静的サイト」を扱うことが多い。この時、単純にHTML/JSだけで外部API(OpenAI等)を叩くと、APIキーをブラウザに渡す必要があり、APIキーの内容が漏洩する。また、これは単純にAPIキーを.env等で環境変数に入れても、フロントのJSに埋め込まれるだけで安全にはならない。

 なので、APIキーをフロントに渡さない工夫が必要になる。

サンプルコード

基本構成(最小)

  • フロント:静的ファイル(HTML/CSS/JS)
  • サーバー:PHPで /api/*.php を置く
  • 秘密情報:APIキーはサーバ側(.env など)にのみ保持

 フロントは外部APIではなく、自分のサーバのPHPにだけリクエストする(例:fetch(“/api/chat.php”))。

仕組み

  1. ブラウザ → /api/search.php にPOST(検索文など)
  2. PHPがサーバ内のAPIキーで外部APIへ中継
  3. PHPが結果だけ返す(キーは絶対に返さない)

サンプル:chatGPT APIを中継して結果を返すだけの最小コード

ディレクトリ例

/public
 /api/chat.php
/.env
/vendor

.env

OPENAI_API_KEY=sk-xxxxxxxxxxxxxxxx

/api/chat.php(最小構成)

<?php
require_once __DIR__ . '/../../vendor/autoload.php';

Dotenv\Dotenv::createImmutable(__DIR__ . "/../..")->load();

header("Content-Type: application/json; charset=utf-8");

// フロントからの入力を取得
$body = file_get_contents("php://input");
if ($body === false || $body === '') {
    http_response_code(400);
    echo json_encode(["error" => "empty request body"]);
    exit;
}

// APIキー取得
$apiKey = $_ENV['OPENAI_API_KEY'] ?? '';
if ($apiKey === '') {
    http_response_code(500);
    echo json_encode(["error" => "API key not set"]);
    exit;
}

// chatGPT API にそのまま中継
$ch = curl_init("https://api.openai.com/v1/chat/completions");
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST => true,
    CURLOPT_HTTPHEADER => [
        "Content-Type: application/json",
        "Authorization: Bearer {$apiKey}",
    ],
    CURLOPT_POSTFIELDS => $body,
    CURLOPT_TIMEOUT => 30,
]);

$response = curl_exec($ch);

if ($response === false) {
    http_response_code(502);
    echo json_encode(["error" => curl_error($ch)]);
    curl_close($ch);
    exit;
}

$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

// chatGPTのレスポンスをそのまま返す
http_response_code($httpCode);
echo $response;

フロント側(例)

// 送信の作成
const payload = {
  model: "gpt-4o-mini",
  messages: [
    { role: "system", content: "必ずJSONのみ返してください。" },
    { role: "user", content: "こんにちは。" }
  ],
  temperature: 0
};

// 送信と受信
const res = await fetch("/api/chat.php", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify(payload)
});

// 受信物
const data = await res.json();
console.log(data);

このサンプルの位置づけ(重要)

 これはあくまで「APIキーをブラウザに出さない」ことだけを達成する最小例なので、そのままでは実務レベルでは使えないので注意。実務では以下を必ず追加検討する。

最低限追加すべきもの
  • 入力サイズ制限(prompt爆撃対策)

  • レート制限(課金事故防止)

  • エラーメッセージの簡略化(内部情報を返さない)

  • MOCK_MODE(ローカル検証用)

あえてやっていないこと
  • バリデーションの厳格化

  • 認証

  • ログ保存

コメント