D1とWorkersを使用したR2内のファイルに対するカスタムアクセス制御
このチュートリアルでは、シンプルなユーザー名とパスワード認証に基づいてファイルアクセスを制御するTypeScriptベースのCloudflare Workerを作成する方法を概説します。これを実現するために、ユーザー管理にはD1データベースを、ファイルストレージにはR2バケットを使用します。
以下のセクションでは、Cloudflare CLIを使用してWorkerを作成し、D1データベースとR2バケットを作成および設定し、作成したR2バケットから安全にファイルをアップロードおよび取得する機能を実装するプロセスを案内します。
- Sign up for a Cloudflare account ↗.
- Install
npm↗. - Install
Node.js↗.
Node.js version manager
Use a Node version manager like Volta ↗ or nvm ↗ to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.
Workerの開発を開始するには、create-cloudflare CLI ↗を使用します。これを行うには、ターミナルウィンドウを開き、次のコマンドを実行します。
npm create cloudflare@latest -- custom-access-controlyarn create cloudflare@latest custom-access-controlpnpm create cloudflare@latest custom-access-controlFor setup, select the following options:
- For What would you like to start with?, choose
Hello Worldの例. - For Which template would you like to use?, choose
Hello World Worker. - For Which language do you want to use?, choose
TypeScript. - For Do you want to use git for version control?, choose
Yes. - For Do you want to deploy your application?, choose
No(we will be making some changes before deploying).
次に、新しく作成したWorkerに移動します。
cd custom-access-controlWorkerを作成したので、次にD1データベースを作成する必要があります。 これはCloudflareポータルまたはWrangler CLIを通じて行うことができます。 このチュートリアルでは、簡単のためにWrangler CLIを使用します。
D1データベースを作成するには、次のコマンドを実行します。
wranglerのインストールを求められた場合は、yを押して確認し、次にEnterを押します。
npx wrangler d1 create <YOUR_DATABASE_NAME><YOUR_DATABASE_NAME>をデータベースに使用したい名前に置き換えてください。この名前は後で変更できないことに注意してください。
データベースが正常に作成されると、バインディングのデータが出力として表示されます。
バインディング宣言は[[d1_databases]]で始まり、バインディング名、データベース名、IDが含まれます。
Workerでデータベースを使用するには、宣言をコピーしてwrangler.tomlファイルに貼り付ける必要があります。以下の例のようにします。
[[d1_databases]]binding = "DB"database_name = "<YOUR_DATABASE_NAME>"database_id = "<YOUR_DATABASE_ID>"D1データベースが作成されたので、アップロードされたファイルを保存するためにR2バケットも作成する必要があります。 このステップもCloudflareポータルを通じて行うことができますが、前と同様にこのチュートリアルではWrangler CLIを使用します。 R2バケットを作成するには、次のコマンドを実行します。
npx wrangler r2 bucket create <YOUR_BUCKET_NAME>これはD1データベースの作成と同様に機能し、<YOUR_BUCKET_NAME>をバケットに使用したい名前に置き換える必要があります。
これを行うには、再度wrangler.tomlファイルに移動し、次の行を追加します。
[[r2_buckets]]binding = "BUCKET"bucket_name = "<YOUR_BUCKET_NAME>"Wranglerの設定を準備したので、worker-configuration.d.tsファイルを更新して新しいバインディングを含める必要があります。
このファイルはTypeScriptにバインディングの正しい型定義を提供し、エディタでの型チェックとコード補完を可能にします。
手動で更新することもできますが、プロジェクトのディレクトリで次のコマンドを実行して、wrangler設定ファイルに基づいて自動的に更新することをお勧めします。
npm run cf-typegenWorkerの開発を開始する前に、D1データベースを準備する必要があります。
これには以下が必要です。
- ユーザーデータを保存するために使用されるテーブルをデータベースに作成する
- ユーザー名列にユニークインデックスを作成し、データベースクエリを高速化し、ユーザー名がユニークであることを保証する
- テストユーザーをテーブルに挿入し、後でコードをテストできるようにする
この操作は一度だけ行う必要があるため、Wrangler CLIを通じて行い、Workerのコード内では行いません。
以下に示すコマンドをコピーし、プレースホルダーを置き換えてから、データベースを準備するために順番に実行します。
このチュートリアルでは、<YOUR_USERNAME>と<YOUR_HASHED_PASSWORD>のプレースホルダーをそれぞれadminと5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8に置き換えることができます。
<YOUR_DATABASE_NAME>は、データベースを作成するために使用した名前に置き換えてください。
npx wrangler d1 execute <YOUR_DATABASE_NAME> --command "CREATE TABLE user (id INTEGER PRIMARY KEY NOT NULL, username STRING NOT NULL, password STRING NOT NULL)" --remotenpx wrangler d1 execute <YOUR_DATABASE_NAME> --command "CREATE UNIQUE INDEX user_username ON user (username)" --remotenpx wrangler d1 execute <YOUR_DATABASE_NAME> --command "INSERT INTO user (username, password) VALUES ('<YOUR_USERNAME>', '<YOUR_HASHED_PASSWORD>')" --remoteデータベースとバケットがすべて設定されたので、Workerアプリケーションの開発を開始できます。 最初に行う必要があるのは、リクエストの認証を実装することです。
このチュートリアルでは、ユーザー名とパスワード(ハッシュ化されたもの)をD1データベースに保存するシンプルなユーザー名とパスワード認証を使用します。
リクエストには、ユーザー名とパスワードをbase64エンコードした文字列が含まれ、これをBasic Authenticationと呼びます。
リクエストメソッドに応じて、この文字列はPOSTリクエストの場合はAuthorizationヘッダーから、GETリクエストの場合はAuthorization検索パラメータから取得されます。
認証を処理するには、index.tsファイル内の現在のコードを次のコードに置き換える必要があります。
export default { async fetch( request: Request, env: Env, ctx: ExecutionContext, ): Promise<Response> { try { const url = new URL(request.url); let authBase64; if (request.method === "POST") { authBase64 = request.headers.get("Authorization"); } else if (request.method === "GET") { authBase64 = url.searchParams.get("Authorization"); } else { return new Response("Method Not Allowed!", { status: 405 }); } if (!authBase64 || authBase64.substring(0, 6) !== "Basic ") { return new Response("Unauthorized!", { status: 401 }); }
const authString = atob(authBase64.substring(6)); const [username, password] = authString.split(":"); if (!username || !password) { return new Response("Unauthorized!", { status: 401 }); }
// TODO: ユーザー名とパスワードが正しいか確認する } catch (error) { console.error("エラーが発生しました!", error); return new Response("Internal Server Error!", { status: 500 }); } },};上記のコードは、リクエストからユーザー名とパスワードを抽出しますが、まだユーザー名とパスワードが正しいかどうかは確認していません。
ユーザー名とパスワードを確認するには、パスワードをハッシュ化し、指定されたユーザー名とハッシュ化されたパスワードでD1データベーステーブルuserをクエリする必要があります。
ユーザー名とパスワードが正しければ、D1からレコードを取得します。ユーザー名またはパスワードが間違っている場合は、undefinedが返され、401 Unauthorizedレスポンスが送信されます。
この機能を追加するには、前のコードスニペットのTODOコメントを次のコードに置き換えます。
const passwordHashBuffer = await crypto.subtle.digest( { name: "SHA-256" }, new TextEncoder().encode(password),);const passwordHashArray = Array.from(new Uint8Array(passwordHashBuffer));const passwordHashString = passwordHashArray .map((b) => b.toString(16).padStart(2, "0")) .join("");
const user = await env.DB.prepare( "SELECT id FROM user WHERE username = ? AND password = ? LIMIT 1",) .bind(username, passwordHashString) .first<{ id: number }>();if (!user) { return new Response("Unauthorized!", { status: 401 });}
// TODO: アップロード機能を実装するこのコードは、すべてのリクエストが処理される前に認証されることを保証します。
認証が設定されたので、Workerを通じてファイルをアップロードする機能を実装できます。
これを行うには、HTTP POSTリクエストを処理する新しいコードパスを追加する必要があります。
その中で、リクエストのデータを取得する必要があります。これはリクエストのボディ内で送信され、request.blob()関数を使用します。
その後、env.BUCKET.put関数を使用してデータをR2バケットにアップロードできます。
最後に、クライアントに200 OKレスポンスを返します。
この機能を実装するには、前のコードスニペットのTODOコメントを次のコードに置き換えます。
if (request.method === "POST") { // ユーザーIDの後にスラッシュを付け、その後にURLのパスを付けてR2バケットにファイルをアップロードします await env.BUCKET.put(`${user.id}/${url.pathname}`, request.body); return new Response("OK", { status: 200 });}// TODO: GETリクエストの処理を実装するこのコードにより、Workerを通じてファイルをアップロードできるようになり、R2バケットに保存されます。
Workerアプリケーションをまとめるために、R2バケットからファイルを取得する機能を実装する必要があります。
これは、GETリクエストを処理する新しいコードパスを追加することで行えます。
このコードパス内で、URLのパス名を抽出し、env.BUCKET.get関数を使用してR2バケットからアセットを取得する必要があります。
コードを最終化するには、前のコードスニペットのGETリクエスト処理のTODOコメントを次のコードに置き換えます。
if (request.method === "GET") { const file = await env.BUCKET.get(`${user.id}/${url.pathname.slice(1)}`); if (!file) { return new Response("Not Found!", { status: 404 }); } const headers = new Headers(); file.writeHttpMetadata(headers); return new Response(file.body, { headers });}return new Response("Method Not Allowed!", { status: 405 });このコードにより、Workerアプリケーションに対してGETリクエストが行われたときに、R2バケットからデータを取得して返すことができるようになります。
このCloudflare Workerチュートリアルのコードを完成させたら、Cloudflareにデプロイする必要があります。 これを行うには、アプリケーション用に作成されたディレクトリでターミナルを開き、次のコマンドを実行します。
npx wrangler deploy認証を求められる場合(まだログインしていない場合)やアカウントを選択するように求められることがあります。その後、WorkerはCloudflareにデプロイされます。 デプロイが成功裏に完了すると、Workerが現在アクセス可能なURLとともに成功メッセージが表示されます。
このチュートリアルを終えるために、ファイルをアップロードするためにPOSTリクエストを送信し、その後ファイルを取得するためにGETリクエストを送信してWorkerアプリケーションをテストする必要があります。
これは、curlやPostmanのようなツールを使用して行うことができますが、簡単のためにcurlの使用法を説明します。
次のコマンドをコピーして、{"Hello": "Worker!"}という内容のシンプルなJSONファイルをアップロードするために使用できます。
<YOUR_API_SECRET>をbase64エンコードされたユーザー名とパスワードの組み合わせに置き換え、コマンドを実行します。この例では、YWRtaW46cGFzc3dvcmQ=を使用できます。これはadminとtestにデコードできます。
curl --location '<YOUR_WORKER_URL>/myFile.json' \--header 'Content-Type: application/json' \--header 'Authorization: Basic <YOUR_API_SECRET>' \--data '{ "Hello": "Worker!"}'次のコマンドを実行するか、単にブラウザでURLを開いて、先ほどアップロードしたファイルを取得します。
curl --location '<YOUR_WORKER_URL>/myFile.json?Authorization=Basic%20YWRtaW46cGFzc3dvcmQ%3D'Cloudflare Workers、R2、またはD1についてさらに学びたい場合は、以下のドキュメントを確認できます。