まくろぐ
更新: / 作成:

Cloudflare Worker で作成した Web API を公開するときに、固定の API キーによるアクセス制限をかける方法です。 独自サービス用のバックエンド API など、一般公開しない API の実装で使うことを想定しています。

準備

プロジェクトの作成

Cloudflare Workers のプロジェクトをまだ作っていないときは、wrangler を使って作成 しておきます。

プロジェクトを作成
$ wrangler init hello-api
$ cd hello-api

API キーを生成して登録する

アクセス制御用の独自の API キーを用意して、それを Workers に登録して使います。 次のように、ランダムな文字列を生成して API キーにしてしまえばよいです。 自分で考えた文字列でも構いませんが、第三者が想像しにくい文字列にしてください。

ランダムな API キーを生成
$ openssl rand -base64 32
mmdkR+mMvBUnYeu2sn1kMqlXjK9Q4A0Os3I4M4aiMQs=

用意した API キーは、サーバー側の API_KEY という環境変数にセットすることにします。 開発サーバー用の環境変数は .dev.vars ファイル、本番環境用の環境変数は wrangler secret コマンドで設定します。

.dev.vars(ローカル環境用のシークレット設定)
API_KEY="mmdkR+mMvBUnYeu2sn1kMqlXjK9Q4A0Os3I4M4aiMQs="
Cloudflare Workers 用のシークレット設定
$ wrangler secret put API_KEY
Enter a secret value: **********************

これらの値は、Workers のプログラム内で env.API_KEY で参照することができます。

x-api-key ヘッダーで API キーを送る方法

下記は、クライアントから x-api-key ヘッダーで API キーを送ってもらう場合の実装例です。

素の Cloudflare Worker で実装する場合

シークレット (env.API_KEY) の値と、x-api-key ヘッダーの値が一致しないときにアクセスを拒否するように実装します。

src/index.ts
export default {
  async fetch(req: Request, env: Env, ctx: ExecutionContext): Promise<Response> {
    // Check if the API key is valid
    if (!env.API_KEY) {
      console.error("API_KEY is not set");
      return new Response("The server setup is not complete", { status: 500 });
    }
    const apiKey = req.headers.get("x-api-key");
    if (apiKey !== env.API_KEY) {
      return new Response("Access denied", { status: 403 });
    }

    // API key is valid, return a response
    return new Response("Hello World!");
  },
};

サーバー側で環境変数 (API_KEY) が定義されていないときは Internal Server Error (500) を返し、クライアントから正しい x-api-key ヘッダーが送られてこなかったときは Forbidden (403) を返ようにしています。

☝️ env の型が不明なとき TypeScript を使っているときに、env.API_KEY の型が不明でエラーになるときは、wrangler types コマンドを実行すると、.dev.varswrangler.toml ファイルの内容から型情報を自動生成してくれます。

pnpm dev (npm run dev) で開発サーバーを起動して、次のようにアクセス制御されていれば成功です。

# x-api-key ヘッダーを送らなかったとき
$ curl localhost:8787
Access denied

# x-api-key ヘッダーを送ったとき
$ curl -H "x-api-key: ..." localhost:8787
Hello World!

Hono フレームワークを使って実装する場合

Hono フレームワークで Web API を作っている場合もほぼ同様に実装できます(参考)。 アクセス制御する部分を次のようにミドルウェアモジュールとして切り出しておくと分かりやすくなります。

src/middleware.ts
import { createMiddleware } from "hono/factory";

interface Bindings {
  API_KEY: string;
}

/**
 * Middleware to check if the corect API key is received.
 */
export const apiKeyMiddleware = createMiddleware<{ Bindings: Bindings }>(
  async (c, next) => {
    if (!c.env.API_KEY) {
      console.error("API_KEY is not set");
      return c.json({ message: "The server setup is not complete" }, 500);
    }

    if (c.req.header("x-api-key") !== c.env.API_KEY) {
      return c.json({ message: "Access denied" }, 403);
    }

    await next();
  }
);
src/index.ts(Hono の場合)
import { Hono } from "hono";
import { apiKeyMiddleware } from "./middleware";

const app = new Hono();
app.use(apiKeyMiddleware);
app.get("/", (c) => {
  return c.json({ message: "Hello" });
});

export default app;

メインロジックがすっきりするのがいいですね!

Authorization ヘッダーで API キーを送る方法

独自ヘッダーで送るとのほとんど変わらないですが、Authorization: Bearer ... というヘッダーで API キーを送る方法もあります。 Hono フレームワークは、bearerAuth ミドルウェア を標準で用意しているので、これを利用すると楽に実装できます。

src/index.ts
import { Hono } from "hono";
import { bearerAuth } from "hono/bearer-auth";

type Bindings = {
  API_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

app.use(async (c, next) => {
  const auth = bearerAuth({ token: c.env.API_KEY });
  return auth(c, next);
});

app.get("/", (c) => {
  return c.json({ message: "Hello" });
});

export default app;
Bearer トークンを使ってアクセス
$ curl -H "Authorization: Bearer ..." localhost:8787
{"message":"Hello"}

ただ、Authorization: Bearer ... ヘッダーは、主に OAuth のシーケンスなどでアクセストークンを送るときに使われるヘッダーなので、API キーを送るときに使うのは少し違和感があるかもしれません。 そんなときは、前述の x-api-key 独自ヘッダーを使っておけばよいです。 ちなみに、最近は x- プレフィックスではなく、{サービス名}-api-key のように、独自サービスの名前をプレフィックスに付けることが推奨されているようです。

関連記事

更新: / 作成:

Hono とは

Hono は、Cloudflare Workers で使える Web アプリ用のフレームワークで、軽量な Web API を実装するときに便利です。 Node.js の Express に似た API を提供しており、ルーティングやミドルウェアを少ないコードで実装することができます。 Cloudflare Workers の Runtime API だけでも Web API を実装できますが、Hono を使うとよりシンプルなコードで実装できます。

☝️ Cloudflare Workers ≠ Node.js JavaScript のランタイム環境としては Node.js が有名ですが、Cloudflare Workers は V8 エンジンをベースとした 独自の Workers ランタイム環境 です。 ローカルでの開発中には Node.js のツール群を使ったりするので余計に混乱しますが、デプロイするコードは Cloudflare Workers 上で動作することを意識して実装する必要があります。 具体的には、Node.js の Runtime API は使えず、Cloudflare Workers の Runtime API を使う必要があります(参考: Workers - Node.js compatibility)。 Hono はもともと Cloudflare Workers 上で動作させることを考えて作られているので安心して使えます。 Web 標準機能を使った実装をウリにしており、Deno Deploy、AWS Lambda など他の環境でも動かすことができます。

Hono で Hello World

Hono プロジェクトの作成

Hono 用のプロジェクトは npm create で作れるようになっているので、基本的にはこれを使ってサクッと作ります(参考: Hono - Getting Started)。

Hono プロジェクトの作成
$ npm create hono@latest my-hono-app   # npm の場合
$ pnpm create hono@latest my-hono-app  # pnpm の場合

ウィザード形式で想定環境を聞かれるので、Cloudflare Workers で動かしたい場合は次のような感じで答えていきます。

  • Which template do you want to use? cloudflare-workers
  • Do you want to install project dependencies? yes
  • Which package manager do you want to use? pnpm

Cloudflare Workers 用の wrangler コマンドの設定ファイル (wrangler.toml) も自動生成されます。

$ tree -L 1
.
├── README.md
├── node_modules
├── package.json
├── pnpm-lock.yaml
├── src
├── tsconfig.json
└── wrangler.toml

開発サーバーで動作確認

プロジェクトを作成したら、開発用サーバーを立ち上げて動作確認します(内部的に wrangler dev が実行されます)。

開発用サーバーの起動
$ npm run dev  # npm の場合
$ pnpm dev     # pnpm の場合

続けて b キーを押すと、Web ブラウザが開きます。 Hello Hono! と表示されれば成功です。

Cloudflare Workers へデプロイ

動作確認が終わったら Cloudflare Workers にデプロイしてみます(内部的に wrangler deploy --minify が実行されます)。

デプロイ
$ npm run deploy   # npm の場合
$ pnpm run deploy  # pnpm の場合(`pnpm deploy` じゃないので注意)

自動的に https://my-hono-app.<ACCOUNT>.workers.dev といった URL が発行されるので、Web ブラウザでアクセスできれば成功です。 Cloudflare のダッシュボード を開くと、デプロイされていることを確認できます。 必要なくなったら wrangler delete でサクッと削除できます。

実装例

src/index.ts
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => {
  return c.text("Hello Hono!");
});

export default app;

上記は自動生成された最小限の実装です。 あとはこのコードをベースにして、Hono のドキュメント(例: Examples / API / Guides)を参考にして少しずつ機能を追加していけば OK です。 以下にいくつか実装例を抜粋しておきます。

テキスト/JSONデータを返す

app.get("/text", (c) => {
  // 自動でレスポンスヘッダー `Content-Type: text/plain` が付与される
  return c.text("Hello");
});

app.get("/json", (c) => {
  // 自動でレスポンスヘッダー `Content-Type: application/json` が付与される
  return c.json({ ok: true, message: "Hello" });
});

パスパラメーター/クエリパラメーターを取得

// Path params
app.get("/books/:id", (c) => {
  const id = c.req.param("id");
  return c.text(`Book-${id}`);
});

// Query params
app.get("/search", async (c) => {
  const query = c.req.query("q");
});

// Get all params at once
app.get("/search", async (c) => {
  const { q, limit, offset } = c.req.query();
});

POST メソッドでデータを受け取る

app.post("/posts", async (c) => {
  // Body で送られてきたデータをパースする(型パラメーターの指定も可能)
  const body = await c.req.parseBody();
  const title = body.title;
  const message = body.message;

  if (!title || !message) {
    return c.json({ message: "Title and message are required" }, 400);
  }
  console.log(`Received: ${title}, ${message}`);
  return c.json({ message: "Created" }, 201);
});
curl で POST リクエストを送信
$ curl -d 'title=Title-1' -d 'message=Message-1' localhost:8787/posts
{"message":"Created"}

$ curl -X POST localhost:8787/posts
{"message":"Title and message are required"}

JSON 形式で送られてきたデータを扱うには次のようにします。

interface PostBody {
  title: string;
  message: string;
}

app.post("/posts", async (c) => {
  const body = await c.req.json<PostBody>();
  const title = body.title;
  const message = body.message;
  // ...
});
curl で POST リクエストを送信(JSON 形式)
$ curl --json '{"title":"Title-1", "message":"Message-1"}' localhost:8787/posts
{"message":"Created"}

外部 API を呼び出す (fetch)

// ポケモンの情報を取得するエンドポイント(例: /pokemon/pikachu)
app.get("/pokemon/:id", async (c) => {
  // Get the `id` path parameter and encode it to prevent injection
  const id = c.req.param("id");
  const safeId = encodeURIComponent(id);

  // Call the PokeAPI
  const res = await fetch(`https://pokeapi.co/api/v2/pokemon/${safeId}`);
  if (!res.ok) {
    const errorText = await res.text();
    console.log(`Pokemon API error: ${errorText}`);
    return c.json({ message: errorText }, 500);
  }

  const data = (await res.json()) as Record<string, unknown>;
  return c.json(data);
});

Header をセットする

c.header("X-Message", "Hello!");

シークレット情報を設定&参照する

Cloudflare Workers では、Bindings という仕組みでシークレット情報(外部 API のキーなど)を参照できるようになっています。 下記は、HOGE_API_KEY というシークレット情報を定義して、Hono アプリからその値を参照する方法です。

開発環境 (wrangler dev) で使うシークレット情報は、.dev.vars というファイルで定義しておきます。 このファイルは .gitignore に登録されていて Git にコミットされないようになっています。

.dev.vars
HOGE_API_KEY="NI6IkpXeiI...省略...leiOiJUzI1"

本番環境 (Cloudflare Workers) にシークレット情報を登録するには、wrangler secret コマンドを使います。 Cloudflare のダッシュボードでも設定できますが、コマンドを使った方が楽です。

シークレット情報の登録
$ wrangler secret put HOGE_API_KEY
Enter a secret value: **********************

シークレット情報は下記のように c.env.変数名 で参照できます。 ここでは、シークレット情報が取得できない場合に 500 エラーを返すようにミドルウェア実装を追加しています。

import { Hono } from "hono";

// 環境変数やシークレットの型定義
type Bindings = {
  HOGE_API_KEY: string;
};

const app = new Hono<{ Bindings: Bindings }>();

// 必要な環境変数が設定されていない場合は 500 エラーを返すミドルウェア
app.use(async (c, next) => {
  if (!c.env.HOGE_API_KEY) {
    console.error("HOGE_API_KEY is not set");
    return c.json({ message: "The server setup is not complete" }, 500);
  }
  await next();
});

// 各ハンドラーの中でシークレットを参照
app.get("/", (c) => {
  const apiKey = c.env.HOGE_API_KEY;
  const masked = apiKey.substring(0, 4) + "****";
  return c.json({ message: `API key: ${masked}` });
});

export default app;

(アプリ用の)環境変数を設定&参照する

シークレット情報ではない、公開可能な環境変数は wrangler.toml の中の [vars] セクションで定義します。

☝️ システムの環境変数ではない ここで言う環境変数というのは、あくまで Workers のコード(Hono のアプリ)から参照可能な変数のことであり、wrangler コマンドに渡されるシステム環境変数 とは異なることに注意してください。 開発中の PC に設定されているシステム環境変数を Workers のコードから参照することはできません。
wrangler.toml
# ...
[vars]
API_BASE_URL = "https://api.example.com/"

参照方法は、シークレット情報と同じく c.env.変数名 です。

src/index.ts
import { Hono } from "hono";

// 環境変数やシークレットの型定義
type Bindings = {
  API_BASE_URL: string;
};

const app = new Hono<{ Bindings: Bindings }>();

// 必要な環境変数が設定されていない場合は 500 エラーを返すミドルウェア
app.use(async (c, next) => {
  if (!c.env.API_BASE_URL) {
    console.error("API_BASE_URL is not set");
    return c.json({ message: "The server setup is not complete" }, 500);
  }
  await next();
});

// 各ハンドラーの中で環境変数を参照
app.get("/", (c) => {
  return c.json({ message: `API_BASE_URL: ${c.env.API_BASE_URL}` });
});

export default app;

特定の環境で別の値を使いたい場合は、wrangler.toml の中で、[env.<環境名>.vars] というセクションを作って環境変数を定義します。 次の例では、local という環境用の環境変数を定義しています。

wrangler.toml
[vars]
API_BASE_URL = "https://api.example.com/"

[env.local.vars]
API_BASE_URL = "http://localhost:3000/"

環境を切り替えて開発サーバーを起動するには、wrangler dev コマンドに --env <環境名> オプションをつけて起動します。 npmpnpm のスクリプト経由で起動するときは以下のような感じで起動します。

開発用サーバーの起動(local という名前の環境で起動)
$ npm run dev -- --env local  # npm の場合 (-- を挟むことに注意)
$ pnpm dev --env local        # pnpm の場合

開発環境では常に --env local で起動したいという場合は、次のように package.json の中でスクリプト定義してしまえばよいでしょう。

package.json
{
  "name": "my-hono-app",
  "scripts": {
    "dev": "wrangler dev --env local",
    "deploy": "wrangler deploy --minify"
  },
  // ...
}

その他

関連記事

更新: / 作成:

Amazon API Gateway で作成した Web API には、独自の API キーによるアクセス制限 をかけることができます。 これにより、x-api-key ヘッダーが含まれていない HTTP リクエストを拒否することができます。 例えば、AWS 外の特定のバックエンドサービスからのアクセスのみを許可して連携させたいときに便利です。 ここでは、簡単な Hello World API を作成して、API キーによるアクセス制限をかけてみます。

Lambda 関数を作成する

最初に、API 実装として Lambda 関数を作成しておきます。

  1. AWS マネジメントコンソール にサインインして、Lambda サービスを開きます。
  2. 関数の作成 をクリックして、一から作成 (Author from scratch) で次のように作成します。
    • 関数名 (Function name): hello
    • ランタイム (Runtime): Node.js 22.x
    • アーキテクチャ (Architecture): arm64 (intel アーキテクチャより少し安い)

次のような Lambda 関数のコードが生成されたら成功です。

index.mjs
export const handler = async (event) => {
  // TODO implement
  const response = {
    statusCode: 200,
    body: JSON.stringify('Hello from Lambda!'),
  };
  return response;
};

API Gateway で Web API を作成する

次に、API Gateway で API を作成して、上記の Lambda 関数を HTTP 経由で呼び出せるようにします。

  1. AWS マネジメントコンソール にサインインして、API Gateway サービスを開きます。
  2. API の作成 をクリックして、次のように作成します。
    • API タイプ: REST APIHTTP API だと API キーが設定できないっぽい)
    • API の詳細: 新しい API
    • API 名: hello-api
  3. API: hello-api のリソース のページから、メソッドの作成 をクリックして、次のように作成します。
    • メソッドタイプ: GET
    • 統合タイプ: Lambda 関数
    • Lambda プロキシ統合: ON
    • Lambda 関数: 先ほど作成した hello 関数を選択
  4. API のデプロイ ボタンをクリックして API をデプロイします。
    • 初回は新しいステージ名を入力して作成します(ここでは prod とします)。

以下のような URL が発行されるので、Web ブラウザや curl コマンドでアクセスして、“Hello from Lambda!” と表示されればデプロイ成功です。

API エンドポイント
https://abcde12345.execute-api.ap-northeast-1.amazonaws.com/prod/

URL の先頭の 10 文字は、作成した API の ID に読み替えてください。

API キーによるアクセス制限をかける

API Gateway で API キーによるアクセス制限をかけるには、次のような構成を作成する必要があります。 はっきりいって分かりにくいです(^^;

  • 各 API リソースの設定で API キーの必須設定 を ON にする
  • API Gateway 上で 使用量プラン を作成し、API ステージhello-apiprod など)を関連づける
  • API Gateway 上で API キー を作成し、「使用量プラン」に関連づける

図にすると下記のような感じ。 カッコの中は、今回設定する値を示しています。

graph LR A["API キー
(HELLO_API_KEY)"] --- B["使用量プラン
(HELLO_API_PLAN)"] B-->C["API ステージ
(hello-api/prod)"]
図: API キーと API ステージの関係

まず、次のように設定して、API 呼び出し時の API キー(x-api-key ヘッダー)の指定を必須にします。

  1. API Gateway の API メニューから、作成した hello-api を選択します。
  2. 各リソースのメソッド(GET など)を選択し、メソッドリクエストの設定の 編集 をクリックします。
  3. API キー必須 を ON にして 保存 します。
  4. API をデプロイ ボタンを押して、再度 API をデプロイします。

これで、単純な HTTP リクエストが次のように弾かれるようになります。

API キーが指定されていないのでエラー
$ curl https://abcde12345.execute-api.ap-northeast-1.amazonaws.com/prod/
{"message":"Forbidden"}

次に、API キーを作成して、使用量プラン(API ステージ)と関連づけます。

  1. API Gateway の 使用量プラン メニューで、使用量プランを作成 をクリックし、次のように作成します。
    • 名前: HELLO_API_PLAN
    • スロットリング: OFF にするか適当に設定
    • クォータ: OFF にするか適当に設定
  2. 使用量プラン HELLO_API_PLAN のページで、API ステージを追加 をクリックし、次のように設定します。
    • API: hello-api を選択
    • ステージ: prod を選択
  3. API Gateway の API キー メニューで、API キーの作成 をクリックし、次のように作成します。
    • 名前: HELLO_API_KEY
    • APIキー: 自動生成
  4. API キー HELLO_API_KEY のページで、使用量プランに追加 をクリックし、次のように設定します。
    • 使用量プラン: HELLO_API_PLAN

これで、API キーを使って API を呼び出せるようになります。

API キーを指定した場合
$ API_KEY=jQjvIkSylW.......(省略).......Py6WFBxJMa
$ curl https://abcde12345.execute-api.ap-northeast-1.amazonaws.com/prod/ -H "X-API-KEY:$API_KEY"
"Hello from Lambda!"

できた! ٩(๑❛ᴗ❛๑)۶ わーぃ

関連記事

メニュー

まくろぐ
サイトマップまくへのメッセージ