AngularサイトにアクセスするクローラーがJavaScriptを実行しない場合、AngularのサービスやHTTP interceptorでは、その最初のリクエストを観測できません。今回はAngular SSRの前段にWebDecoyを置き、ページの表示を続けながらリクエストを確認します。
WebDecoy開発元による解説です。執筆・翻訳にはAIを使用しています。対象はWebDecoyのリクエスト検知SDKであり、FCaptchaではありません。
構成
ブラウザーやクローラー → Express → WebDecoy → Angular SSR
ミドルウェアは、公開カタログAPIとAngularのレンダリング処理の両方より前に置きます。ブラウザー側にAPIキーや拒否判定を持たせません。
静的ホストでAngularのファイルを配信し、APIだけ別のサーバーで動かしている場合、APIサーバーに追加するだけではページへのアクセスは見えません。配信元やCDNで観測する必要があります。CDNがNodeに転送せず返すキャッシュ応答も、この構成の対象外です。
実行用サンプル
今回の確認にはNode.js 26.5を使用しました。Angularは22.2、@webdecoy/expressと@webdecoy/nodeは0.18.0です。
git clone https://github.com/WebDecoy/node.git
cd node
git checkout 1a6b0700d45f4437311e359d3ab1ea91927fc0bf
cd examples/angular-monitor/app
npm ci --workspaces=false
npm run build
npm test
npm run serve
http://127.0.0.1:4300を開き、Load public catalogを押すと、公開デモデータを取得できます。
ng serveではなく、ビルド後のNodeサーバーを使います。確認したいのは、本番用サーバー内でのミドルウェアの配置だからです。サンプル全体には、コンポーネント、サーバー、lockfile、結合テストが含まれています。
Angularのハンドラーより前に配置する
src/server.tsでは、angularApp.handle(req)を呼ぶ前に以下を登録します。
import { webdecoy } from '@webdecoy/express';
import { tripwire, rateLimit } from '@webdecoy/node';
app.use(webdecoy({
apiKey: process.env['WEBDECOY_API_KEY'] || undefined,
mode: 'monitor',
honeytoken: false,
rules: [
tripwire({ paths: ['/.env', '/wp-config.php'] }),
rateLimit({ max: 60, window: 60 }),
],
}));
これは既存のExpressサーバーに組み込む部分です。server.ts全体をこのコードで置き換えるものではありません。既存の認証やルーティングは残します。
monitorでは、拒否という判定になっても、リクエストをアプリへ進めます。トリップワイヤーのパスには、本物の環境変数ファイルやWordPress設定ファイルを置いていません。
60秒で60リクエストという制限は検証用です。プロセス内のカウンターなので、複数レプリカ全体で共有される制限ではありません。実運用では適切な値や共有ストアを検討します。
honeytoken: falseはHTMLへの自動トラップ挿入を無効にします。このサンプルではhydration時のDOM変更を増やさず、まずサーバーリクエストの観測に絞っています。
実在する静的ファイルとhealthエンドポイントだけが検知を迂回します。Angularのサーバールートは次の設定です。
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{ path: '**', renderMode: RenderMode.Server },
];
SSR、prerender、CSRの違いはAngularの公式ガイドで確認できます。どの配信経路をリクエストが通るかが、観測できる範囲を決めます。
判定とHTTP応答を分けて確認する
別のターミナルから実行します。
curl -i http://127.0.0.1:4300/api/products
curl -i http://127.0.0.1:4300/.env
カタログはHTTP 200と2件のデモ商品を返します。/.envへの要求では、ターミナルにconclusion: "DENY"、tripwire: true、wouldBlock: trueが記録されます。
それでもHTTP応答は404です。monitorモードがアプリ側の応答を維持するためです。ログにDENYがあることと、実際にブロックしたことは区別します。
サンプルは限定した判定情報だけを記録し、IP、全ヘッダー、リクエスト本文、認証情報はログに出力しません。
ダッシュボードへの送信を確認する
クラウドで確認する場合は、WebDecoyで作成した自分のキーをサーバー環境変数WEBDECOY_API_KEYに設定し、再起動します。Angularの設定、コンポーネント、interceptorにはキーを埋め込まないでください。
curl -i -A 'WebDecoy-Test/1.0' http://127.0.0.1:4300/api/products
Detections画面でTestラベルの記録を確認します。これは予約された検証用User-Agentであり、本物のAIクローラーによるアクセスではありません。
キーがない場合、サンプルはdashboardConfigured: falseとreportingError: trueを明示します。キーを設定しただけでも到達確認にはならないので、画面の記録と送信エラーを確認してください。
実行確認の結果
本番ビルドと以下の5件の結合テストが成功しました。
| 確認 | 結果 |
|---|---|
| JavaScript実行前のHTMLにAngularの内容がある | 成功 |
| 公開カタログAPI | HTTP 200、商品2件 |
| トリップワイヤー | DENY判定、アプリの404を維持 |
| 予約されたテスト要求 | ラベルとキー未設定の状態を確認 |
| レート制限の観測 | 拒否判定を記録しつつカタログは200 |
APIキーなしで実行したため、クラウドへの送信や、本物のAIクローラーに対する検知精度を検証した結果ではありません。
デプロイ後に読むポイント
要求パス、分類、根拠となるシグナル、繰り返しの傾向を見ます。User-Agentのクローラー名は自己申告なので、検証済みの身元とは分けて扱います。
サンプルはloopbackにbindし、転送ヘッダーを信頼しません。プロキシの後ろで運用する場合は、その構成に合わせて信頼するプロキシを設定します。誤ると複数の訪問者を同じIPとして扱ってしまいます。
検知一覧は完全なアクセスログでもありません。ローカルで許可され送信されない要求や、キャッシュの影響があります。全リクエスト数の集計にはアクセスログを併用してください。
まずmonitorのまま観測すると、サイトを止めずに、どのアクセスに対応する必要があるかを確認できます。