CORSプリフライトの深淵:OPTIONSメソッドとRTTの「0.5往復」を制するアーキテクチャ設計
ネットワークエンジニアの端くれとして、ブラウザが発する OPTIONS メソッドを目にすると、私はいつも「またしてもレイテンシの呪縛が始まったか」と溜息をつきたくなる。
Web APIの設計において、CORS(Cross-Origin Resource Sharing)はセキュリティの要だ。しかし、インフラアーキテクトの視点で見れば、この「プリフライトリクエスト」こそが、パフォーマンスとセキュリティの果てしない綱引きの象徴に他ならない。今回は、この OPTIONS がパケットレベルで何を行い、我々がそれをどう御すべきか、泥臭いレイヤーから紐解いていこう。
1. プリフライトのパケット挙動と「RTTの呪い」
ブラウザが「安全ではない」と判断したリクエスト(Content-Type: application/json を含むPOSTなど)を行う際、ブラウザは本番の通信の前に OPTIONS メソッドを投げる。これがプリフライトだ。
このとき、ネットワーク上では何が起きているか。
1. TCP 3-way Handshake: 新たな接続であれば、まずSYN/SYN-ACK/ACK。
2. TLS Handshake: 鍵交換、証明書検証。もしTLS 1.3であれば1往復だが、旧いプロトコルならさらに往復が増える。
3. OPTIONSリクエスト: ブラウザが Access-Control-Request-Method などを送信。
4. OPTIONSレスポンス: サーバーが Access-Control-Allow-Origin を返送。
5. 本番リクエスト: ようやく目的のPOSTが送信される。
この一連の流れにより、クライアントは本番リクエストの前に少なくとも1回の「空の往復」を強いられる。グローバル環境でのクライアントとサーバーの物理的距離が100msのRTTを持つなら、APIの応答時間は最低でも100ms、環境によってはそれ以上に水増しされる。これが、我々が直面する「0.5往復(プリフライト分)の損失」の正体だ。
2. インフラレベルでの最適化:RTTとTCPスタックの調律
このRTTを削るには、アプリケーション層よりも下位層、すなわちカーネルとネットワーク構成の最適化が不可欠となる。
TLS 1.3への強制移行
TLS 1.3ではハンドシェイクが1往復で完了する。さらに 0-RTT (Zero Round-Trip Time) を活用すれば、クライアントが前回のセッション情報を保持している場合、暗号化ハンドシェイクと同時にデータを送信できる。nginxであれば以下の設定が基本だ。
# TLS 1.3を優先し、ハンドシェイクのオーバーヘッドを最小化
ssl_protocols TLSv1.2 TLSv1.3;
ssl_early_data on; # 0-RTTを有効化(※リプレイ攻撃対策を別途講じること)
TCPバッファとKeep-Alive
プリフライトと本番リクエストを「同一のTCPコネクション」上で処理させることが重要だ。Keep-Alive が切れていると、プリフライトごとに再接続が発生し、TCPのSlow StartとTLSハンドシェイクが毎回繰り返されるという悪夢が待っている。
# TCP接続を維持し、プリフライトと本番リクエストを相乗りさせる
keepalive_timeout 65;
keepalive_requests 1000;
3. Access-Control-Max-Age による「キャッシュ」の極意
プリフライトの回数を減らす最強の策は、Access-Control-Max-Age ヘッダーを適切に設定することだ。これは「このプリフライトの結果をブラウザ側で何秒間キャッシュするか」を指示する。
多くのプロジェクトでここがデフォルト(0秒)のまま放置されているが、これはインフラエンジニアとしては失格だ。
# 1時間(3600秒)キャッシュさせる例
Access-Control-Max-Age: 3600
これを設定することで、2回目以降の同一リクエストでは、ブラウザはプリフライトを省略して直接本番リクエストを投げるようになる。これにより、RTTを劇的に削減可能だ。
4. セキュリティとパフォーマンスの均衡点
最後に、Access-Control-Allow-Origin の設定について。しばしば * を指定して済ませるケースを見かけるが、これは認証情報(Cookie や Authorization ヘッダー)を伴う通信では機能しない。
認証が必要なAPIでは、以下のように「動的なドメイン検証」を行うのが鉄則だ。
// PHPでの実装例:許可リストに基づく動的レスポンス
$allowed_origins = ['https://app.example.com', 'https://admin.example.com'];
$origin = $_SERVER['HTTP_ORIGIN'] ?? '';
if (in_array($origin, $allowed_origins)) {
header("Access-Control-Allow-Origin: $origin");
header("Access-Control-Allow-Credentials: true");
header("Access-Control-Allow-Methods: GET, POST, OPTIONS");
header("Access-Control-Allow-Headers: Authorization, Content-Type");
header("Access-Control-Max-Age: 86400"); // 24時間キャッシュ
}
結びに代えて
プリフライトリクエストは、単なる「CORSの通過儀礼」ではない。それは、クライアントとサーバーの間の物理的距離と、TCP/TLSというプロトコルの制約を可視化する鏡だ。
パケットが光速で駆け巡る世界において、わずか数msの無駄を削り出すことこそが、我々インフラアーキテクトの矜持である。OPTIONS を見かけたら、それが「無駄なパケット」ではなく「最適化の余地があるサイン」であると捉えてほしい。
あなたのサーバーが刻むTCPストリームが、今日もミリ秒単位で美しく最適化されていることを願う。
コメント