はじめに(なぜこれを作ったか)
こんにちは。
WASM(WebAssembly) が気になったので、勉強を兼ねて小さなサンプルを作ってみました。
このサンプルでやりたかったことは、とてもシンプルです。
明らかに不正なリクエストは、
バックエンドに届く前に止めたい
という考え方を、できるだけ小さな構成で形にすることです。
従来のAPI構成の問題点
多くのWeb APIでは、次のような流れが一般的です。
- クライアントからリクエストが送られる
- バックエンドのAPIサーバがリクエストを受け取る
- バックエンドで認証や検証を行う
- 不正であればエラーを返す
この構成自体は間違っていません。
ただしこの方法では、不正なリクエストであっても、毎回バックエンドまで到達してしまうという問題があります。
リクエストが増えれば増えるほど、バックエンド側の負荷も増えていきます。
発想を変える:通す前に落とす
そこで発想を少し変えてみます。
そもそも通してはいけないリクエストは、
バックエンドに届く前に止められないか?
この考え方を実現するために使うのが、Edge と WASM です。
Edge とは何か
ここで言う Edge とは、
クライアントからのリクエストを、
バックエンドよりも手前で受け取る場所
のことです。
Edge では次のような処理を行います。
- HTTPリクエストを受け取る
- Authorization ヘッダなどを読み取る
- 処理を続けるか、ここで止めるかを判断する
- 必要であれば、その場でレスポンスを返す
今回のサンプルでは、Cloudflare Workers 本番環境そのものではなく、
ローカル環境で動作する「Edge的な実行環境」を使っています。
Edge という言葉に馴染みがなくても、
「バックエンドの一歩手前で処理する場所」
と考えてもらえれば問題ありません。
なお、Edge という実行モデルについては、以下の記事がとても分かりやすいです。
https://zenn.dev/moutend/articles/97c98a277f4bae
WASM とは?
一方で WASM(WebAssembly) は、まったく別の役割を持ちます。
WASM は、
- HTTPを知らない
- リクエストという概念を持たない
- ヘッダの存在も知らない
という特徴があります。
このサンプルで WASM が行うことは、たった一つです。
渡された文字列が、
正しい JWT かどうかを判定する
JWT の検証ロジックだけを WASM に閉じ込め、
それ以外のことは一切させません。
なぜ役割を分けるのか
このサンプルでは、役割を次のように分けています。
Edge の責務
- HTTPリクエストの受付
- ヘッダの取得
- リクエストを通すか止めるかの判断
WASM の責務
- JWTが正しいかどうかの判定ロジックのみ
バックエンドの責務
- すでに検証を通過したリクエストだけを処理
このように分離することで、
- HTTP処理と認証ロジックが混ざらない
- 認証ロジックを他の環境でも再利用しやすい
- 不正なリクエストでバックエンドを無駄に呼ばなくて済む
といったメリットがあります。
このサンプルの位置づけ
Edge や WASM をまだ触ったことがない人でも(自分自身も含めて)
- なぜこの構成にするのか
- どこで何をやっているのか
がイメージできることを目指し、できるだけ小さく、シンプルな構成にしています。
「Edge と WASM を使うと、こういう分離ができるのか」という
入り口として読んでもらえたら嬉しいです。
全体構成
Client → Edge(Cloudflare Workers)→(必要なら)Origin
役割分担
Edge(TypeScript): HTTP受付 / Header抽出 / 制御
WASM(Rust): JWT検証ロジックのみ
API仕様(最小)
Request
GET /protected
Authorization: Bearer
Response
OK: 200 OK
NG: 401 Unauthorized
重要:責務分離(設計の芯)
❌ WASMにやらせないこと
- HTTPヘッダ取得
- Cookie処理
- Authorization: Bearer ... のパース
- リクエスト拒否判断(ステータス / レスポンス生成)
✅ WASMにやらせること
- JWT文字列 → 正しいか?
- HS256署名検証
- exp / aud / iss 検証
- 結果を 数値(i32)で返す
リポジトリ構成(最小)
edge-wasm-smart-auth/
├─ worker/
│ ├─ worker.ts
│ ├─ auth.wasm
│ ├─ wasm.d.ts
│ └─ wrangler.toml
├─ wasm/
│ ├─ Cargo.toml
│ └─ src/lib.rs
└─ README.md
WASM(Rust):JWT検証「だけ」を実装する
HS256前提で jsonwebtoken を使って検証する。
返り値は Ok / Expired / Invalid に拡張している。
lib.rs
use std::{mem, slice, str};
use jsonwebtoken::{decode, Algorithm, DecodingKey, Validation};
use serde::Deserialize;
// 最小構成: HS256 共有鍵(本番は Edge から渡す/Secrets 管理を推奨)
static SECRET: &[u8] = b"super-secret-key";
static AUDIENCE: &str = "my-audience";
static ISSUER: &str = "my-issuer";
#[derive(Debug, Deserialize)]
struct Claims {
sub: String,
exp: usize,
aud: String,
iss: String,
}
// Edge 側が token bytes を置くための領域を確保する。
// - WASMはHTTPを知らない(bytesの受け渡しのみ)
// - Edgeは検証ロジックを知らない(validate_tokenの結果のみ)
#[no_mangle]
pub extern "C" fn alloc(len: usize) -> *mut u8 {
let mut buf = Vec::<u8>::with_capacity(len);
let ptr = buf.as_mut_ptr();
// Rust側で解放されないようにリークさせ、deallocで回収する
mem::forget(buf);
ptr
}
// alloc で確保した領域を解放する。
#[no_mangle]
pub extern "C" fn dealloc(ptr: *mut u8, len: usize) {
unsafe {
// capacity=len で確保した前提(Edgeが同じlenで呼ぶ)
drop(Vec::<u8>::from_raw_parts(ptr, 0, len));
}
}
#[repr(i32)]
pub enum AuthResult {
Ok = 1,
Expired = -1,
Invalid = 0,
}
// JWT を検証する(純粋ロジックのみ)
// 戻り値:
// 1 = OK
// 0 = Invalid
// -1 = Expired
#[no_mangle]
pub extern "C" fn validate_token(ptr: *const u8, len: usize) -> i32 {
let bytes = unsafe { slice::from_raw_parts(ptr, len) };
let token = match str::from_utf8(bytes) {
Ok(v) => v,
Err(_) => return AuthResult::Invalid as i32,
};
let mut validation = Validation::new(Algorithm::HS256);
validation.set_audience(&[AUDIENCE]);
validation.set_issuer(&[ISSUER]);
let result = decode::<Claims>(
token,
&DecodingKey::from_secret(SECRET),
&validation,
);
match result {
Ok(_) => AuthResult::Ok as i32,
Err(err)
if matches!(
err.kind(),
jsonwebtoken::errors::ErrorKind::ExpiredSignature
) =>
{
AuthResult::Expired as i32
}
Err(_) => AuthResult::Invalid as i32,
}
}
Edge(Cloudflare Workers)
Edgeは検証の中身を知らない。
知っているのは関数契約(alloc / validate_token / dealloc)のみ。
result に応じてレスポンスを制御する。
switch (result) {
case 1:
return 200 OK
case -1:
return 401 Token Expired
default:
return 401 Unauthorized
}
ビルド(WASM)
rustup target add wasm32-unknown-unknown
cd wasm
cargo build --release --target wasm32-unknown-unknown
起動(Worker)
cd worker
wrangler dev
動作確認
curl -i -H "Authorization: Bearer <JWT>" \
http://127.0.0.1:8787/protected
JWT前提:
aud = my-audience
iss = my-issuer
secret = super-secret-key
実装のポイント
- WASMはHTTPを知らない(bytes → i32)
- WASMは状態を持たない(DB / セッションなし)
- Edgeが制御を握る(許可 / 拒否 / Refresh導線)
- WASMは再利用できる(他Gatewayでも使い回せる)
まとめ
- Edge × WASM の本質は高速化ではない。
- 責務分離・安全性・差し替え可能性を最小構成で実現できる点に価値がある。
サンプルコードです。
ぜひ実際に動かしてみてください。