REST API やフロントエンドフレームワークを使ったウェブアプリケーションを作っていると、CORS (Cross-Origin Resource Sharing) に悩まされることが珍しくありません:
cors.001


CORS とはウェブブラウザ(フロントエンド)の JavaScript におけるセキュリティ制約の1つで、AJAX などを使って HTTP リクエストを行う場合、原則的には同一オリジンからのリクエストだけが許可され、異なるオリジンからのリクエストは CORS 制約によって失敗するようになっています。バックエンドからの HTTP リクエストに関しては一切制約はありませんが、フロントエンドからのリクエストに対してのみ適用されるものです。詳しくはこちらもどうぞ:
https://developer.mozilla.org/ja/docs/Web/HTTP/CORS


ただ REST API とフロントエンド・ウェブアプリケーションを作っている人が同一であったり、そういったリクエストが来るとわかっている前提で REST API を用意する場合など、API 提供側からもこの CORS 制約を無視したくなることがあります。そういった場合のために「REST API 側で特定のオリジンからの HTTP リクエストについては許可する」設定も可能です。具体的には許可するオリジン(https://allowed-origin.com)を指定して、
 Access-Control-Allow-Origin: https://allowed-origin.com
という HTTP レスポンスヘッダを返します。リクエスト元ではこのヘッダの内容を自分自身のオリジンと比較して、一致している場合のみ正常処理とみなす(一致していない場合はエラーとする)という処理が行われます。なお「全てのオリジンからのリクエストを許可する」という指定も可能で、その場合は例外的に
 Access-Control-Allow-Origin: *
という HTTP レスポンスヘッダを返すことで実現できるようになっています。

さて、実際にフロントエンド側を開発していると、以下のような問題に直面することがあります:
(1)開発中やテスト中は http://localhost:3000 で動かす
(2)開発終了後は https://xxxxx.com で実運用する
(3)REST API は(1)とも(2)とも異なる環境で稼働している(つまり CORS 設定しないと動かない)

開発中は自分の PC でコーディングするので http://localhost:3000 のようなローカルホストを使った動作確認となり、実運用中は運用のために用意した https://xxxxx.com というインターネット上のホストを使ってアクセスする、というケースです(特別に珍しいケースではないと思います)。開発(テスト)環境でも実運用環境でも REST API を動かす必要があるため、REST API 側ではいずれのケースでも動作するような CORS 設定ができると楽です(http://localhost:3000 からのリクエストを許可する是非はともかく)。しかし CORS の Access-Control-Allow-Origin ヘッダには1つのオリジン(それも * 以外の正規表現とかは使えずに)しか記述できない、という制約があります。つまり「http://localhost:3000 または https://xxxxx.com からのリクエストを許可する」という Access-Control-Allow-Origin の指定はできないのですが、これをうまく回避・実現する方法はないでしょうか?

1つの考えられる方法として「開発中だけは "Access-Control-Allow-Origin: *" を指定」して全てのオリジンからのリクエストを許可する、という方法も考えられます。が、これはあまりに無防備な設定でもあります。

そこで考えたのが以下の方法です。あらかじめ許可するオリジンを配列として用意した上で、リクエスト元が許可オリジン配列の中に含まれていた場合はそのオリジンを Access-Control-Allow-Origin ヘッダに動的に指定して返す、という方法です。Node.js + Express 環境向けに具体的に作るとこんな感じ:
// app.js
var express = require( 'express' ),
    app = express();

app.use( express.Router() );

var settings_cors = 'CORS' in process.env ? process.env.CORS : '';
app.all( '/*', function( req, res, next ){
  if( settings_cors ){
    var origin = req.headers.origin;
    if( origin ){
      var cors = settings_cors.split( " " ).join( "" ).split( "," );

      //. cors = [ "*" ] への対応が必要
      if( cors.indexOf( '*' ) > -1 ){
        res.setHeader( 'Access-Control-Allow-Origin', '*' );
        res.setHeader( 'Vary', 'Origin' );
      }else{
        if( cors.indexOf( origin ) > -1 ){
          res.setHeader( 'Access-Control-Allow-Origin', origin );
          res.setHeader( 'Vary', 'Origin' );
        }
      }
    }
  }
  next();
});

app.get( '/ping', function( req, res ){
  res.contentType( 'application/json; charset=utf-8' );

  res.write( JSON.stringify( { status: true, message: 'PONG' }, null, 2 ) );
  res.end();
});

var port = process.env.PORT || 8080;
app.listen( port );
console.log( "server starting on " + port + " ..." );


起動時の環境変数 CORS として許可オリジンの配列(以下の例では "http://localhost:3000" と "https://xxxxx.herokuapp.com" の2つ)をカンマ区切りで指定します:
(例)$ CORS=http://localhost:3000,https://xxxxx.herokuapp.com node app

このサンプル(8080 番ポートで起動)では GET /ping というリクエストに { status: true, message: 'PONG' } というレスポンスを返す REST API が定義されていますが、リクエスト元のオリジンが http://localhost:3000 か https://xxxxx.herokuapp.com のいずれかの場合は Access-Control-Allow-Origin ヘッダによって CORS の制約を回避して実行できます。

これでテスト環境、ステージング環境、本番環境などで CORS の設定を変えずに運用できる API サーバーが用意できそうです。


上記サンプルソースコードはこちらに用意しました:
https://github.com/dotnsf/multicors