AWS OpsWorks の nginx の CORS 対応でハマった

AngularJS で作った SPA(Single-page application) から、Ruby on Rails で作った API と情報をやりとりするため、CORS(Cross-Origin Resource Sharing) 対応が必要になり、AWS の OpsWorks の nginx のレシピを修正してハマった話。

CORS(Cross-Origin Resource Sharing) とは

ブラウザで Ajax 通信を行う場合、同一生成元ポリシー(Same Origin Policy)によって、ページの生成元のドメイン以外のドメインへの HTTP リクエストができない。
簡単に言うと、ドメインA(http://domaina.com) で読み込んだ HTML から、ドメインB(http://domainb.jp) への Ajax 通信は制限されているということ。
通信しようとすると、例えば下記のようなエラーが出る。

1
XMLHttpRequest cannot load http://domainb.jp/api/v1/addresses?postal_code=3120003. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://domaina.com' is therefore not allowed access.

ドメインが異なる HTTP リクエスト(クロスサイト HTTP リクエスト)を安全に行えるように作られたのが、CORS である。
CORS とは Cross-Origin Resource Sharing の略で、2014年1月に W3C 勧告となった仕様である。

AWS OpsWorks の nginx レシピを修正

この CORS 対応するため、API 側の nginx の設定を変更してやる必要がある。
※ 本番環境では、同一のドメインになるため、この設定は必要なくなるが、開発環境はローカルの開発環境からステージング環境の API にアクセスする必要があるため、サーバー側の設定が必要になった。

API サーバーは、AWS の OpsWorks を利用しているので、 aws/opsworks-cookbooks を変更して、カスタムレシピとして設定しないといけない。

何か参考にできるものはないかと、ググってみたら こちら が良さそうだったので、これを参考にしつつ、前に別の案件で使った設定と合わせて確認しながら試してみた。

シンプルなリクエストとプリフライトリクエスト

サーバー側の設定を行う際に、リクエストが GET もしくは POST の場合(シンプルなリクエスト)と、リクエストが OPTIONS の場合(プリフライトリクエスト)とを分ける必要がある。

シンプルなクロスサイトリクエストとは

シンプルなリクエストとは、GET または POST のみ用いる。
POST をサーバーへのデータ送信に利用する場合、HTTP POST でサーバーに送られるデータの Content-Type は application/x-www-form-urlencoded、multipart/form-data、または text/plain のいずれかとなる。
例えばサーバー側でアクセスを http://domaina.com からだけに制限したい場合は、以下のように Access-Control-Allow-Origin: ヘッダー を返す必要がある。

Access-Control-Allow-Origin: http://domaina.com

プリフライトリクエストとは

プリフライトリクエストとは、上記のシンプルなリクエスト以外のリクエスト。
シンプルなリクエストと違い、始めに実際のリクエストを送信しても安全かを確かめるために、他ドメインへ向けて HTTP の OPTIONS リクエストヘッダを送信する。

上記を踏まえると、サーバー側では、HTTP メソッドが OPTIONS か、 GETやPOST か を判別して、それぞれレスポンスを返す必要がある。

if 文がうまく動いてない??

はじめに下記のように設定をしてみた。
location @unicorn の部分で $request_method で判別して、OPTIONS だったらプリフライトリクエスト用の設定を、GET(POST) だったらシンプルなリクエスト用のヘッダーの設定を付与する、というもの。

1
location @unicorn {
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_redirect off;

  if ($request_method = 'OPTIONS') {
    add_header 'Access-Control-Allow-Origin' "*";
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since';
    add_header 'Content-Length' 0;
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    return 204;
  }

  if ($request_method = 'GET') {
    add_header 'Access-Control-Allow-Origin' "*";
    add_header 'Access-Control-Allow-Credentials' 'true';
    add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
    add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
  }

  if ($request_method = 'POST') {
    ...
    ...
  }  
  (略)

  # If you don't find the filename in the static files
  # Then request it from the unicorn server
  if (!-f $request_filename) {
    proxy_pass http://unicorn_<%= @application[:domains].first %>;
    break;
  }
}

これがなぜかうまく動かなかった。
if 文の条件にどこにも合致せずに、ヘッダーに設定が入っていない状況だった。
$request_method に、値が入っていないのかと思い、デバッグのため下記の設定を入れてみて確かめてみたものの、

1
add_header 'X-request-method' '$request_method';`

しっかり GET と入っていて……。
切り分けのために、if 文を取り除いてみたらちゃんとヘッダーが設定されてレスポンスされていて……。
そもそも if 文自体が使えない? って思ったけど、もともとの OpsWorks のレシピにはしっかり if 文が入っているし、if (!-f $request_filename) の部分はちゃんと動いているし……。
じゃあ、必ず通るはずの if 文を書いてみて切り分けしてみよう!と思ってやってみたものの、なぜか設定されず……。
ヽ(`Д´#)ノ ムキー!!

最終的にこうなった

いろいろと試行錯誤した結果、下記のような設定でいけた。
location @unicorn で設定する前の、location / の部分で判別するように設定した。

1
location / {
  <% if node['nginx']['enable_cors'] -%>
  if ( $request_method = OPTIONS ) {
    add_header 'Access-Control-Allow-Origin' "*";
    add_header 'Access-Control-Allow-Headers' 'Authorization,Content-Type,Accept,Origin,User-Agent,DNT,Cache-Control,X-Mx-ReqToken,Keep-Alive,X-Requested-With,If-Modified-Since';

    add_header 'Content-Length' 0;
    add_header 'Content-Type' 'text/plain charset=UTF-8';
    return 204;
  }
  <% end -%>
  try_files $uri/index.html $uri/index.htm @unicorn;
}

location @unicorn {
  <% if node['nginx']['enable_cors'] -%>
  add_header 'Access-Control-Allow-Origin' "*";
  add_header 'Access-Control-Allow-Credentials' 'true';
  add_header 'Access-Control-Allow-Methods' 'GET, POST, OPTIONS';
  add_header 'Access-Control-Allow-Headers' 'DNT,X-CustomHeader,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type';
  <% end -%>
  proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
  proxy_set_header Host $http_host;
  proxy_redirect off;

<% if node[:nginx] && node[:nginx][:proxy_read_timeout] -%>
  proxy_read_timeout <%= node[:nginx][:proxy_read_timeout] %>;
<% end -%>
<% if node[:nginx] && node[:nginx][:proxy_send_timeout] -%>
  proxy_send_timeout <%= node[:nginx][:proxy_send_timeout] %>;
<% end -%>

  # If you don't find the filename in the static files
  # Then request it from the unicorn server
  if (!-f $request_filename) {
    proxy_pass http://unicorn_<%= @application[:domains].first %>;
    break;
  }
}

location / の部分で、$request_method が OPTIONS だったら、プリフライトリクエスト用のヘッダーの設定を適用して、204 でレスポンスを返す。
そうでない GET や POST の場合は、シンプルなリクエスト用のヘッダーの設定を付与して Rails へ、という流れ。
ステージング環境用だし、とりあえず動いたのでよしとするも、なぜ if 文が思うように動かなかったのか納得がいかないので引き続き調査中……。

参考サイト

HTTP access control (CORS) | MDN
CORS(Cross-Origin Resource Sharing)によるクロスドメイン通信の傾向と対策 | Developers.IO
CORS(Cross-Origin Resource Sharing)について整理してみた | Developers.IO
CORSでハマったことまとめ - pixiv inside

OpsWorksなどのマネージドサービスをフル活用したAWSインフラ構築や運用を御希望の企業様は、是非MMMにご相談下さいませ!

このエントリーをはてなブックマークに追加