(PHP / CORS) Access-Control-Allow-Origin に複数のオリジンを指定したい

経緯

あるデータリソース(例: JSONファイル)に対して自作の PHP のインターフェースを作成し、 JS(jQuery) でデータの問い合わせを行う仕組みを作りました。

その後、よんどころない事情により開発環境(BrowserSync)からも本番のデータを取得したくなりました。そうすると、リクエストを投げるJSのオリジンは localhost:3000192.168.x.x:3000 のようなホスト名(IP) + ポート番号になると思います。

一方のサーバのオリジンはドメイン(例えば demo.example.jp)なので、当然 CORS に引っかかります。

Access to XMLHttpRequest at ‘https://demo.example.jp/path/to/interface/’ from origin ‘http://localhost:3000′ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

そこで、こんな風にエラーが発生してしまいます。

ここまでは想定内だったのですが、上述の通り開発環境のオリジン、特にホスト名は locahost だったりローカルIPだったりしますし、もっと言えば(仮に同じLAN内だとしても)PCが DHCP で自動的にIPアドレスが振られる環境ならばローカルIPの値が変化することが容易に想像できます。

やりたいことは参照だけなので、一時的なものならば Access-Control-Allow-Origin: * とワイルドカード指定をするという方法もあるのですが、今回はワイルドカードは避けたいと考えました。

そのため、 Access-Control-Allow-Origin に複数のオリジンを指定する方法を調べたのですが……

オリジンを指定します。1つのオリジンだけを指定することができます。サーバーが複数のオリジンからのクライアントに対応している場合、リクエストを行った特定のクライアントのオリジンを返さなければなりません。

Access-Control-Allow-Origin – HTTP | MDN

……複数指定、できないのですね。

調査・実装

いくつかの記事を調べてみたところ、上述の通り「リクエストを行った特定のクライアントのオリジンを返」すという形になりそうです。

そこで、検証のため簡単なプロジェクトを作成しました。

ディレクトリ構造

生成された結果の構造を示します。

dist/
  ├ corspan/                      // テストのための簡単な PHP インターフェース
  │    ├ data/                    // データ
  │    │  └ data.json             // サンプルデータ
  │    ├ src/                     // PHP コード
  │    │  ├ config.php            // 設定 (Origin 条件にマッチさせるオリジンの正規表現文字列の配列が中にあり)
  │    │  ├ ControllerCors.php    // レスポンスヘッダ部: 上記 Origin 条件を判定し、 CORS 関連のヘッダを出力する
  │    │  ├ ControllerJson.php    // レスポンスボディ部: data.json を出力
  │    │  ├ Helpers.php           // ファイル読み込みや JSON エンコード・デコードの関数のラッパーの塊
  │    │  └ ModelGet.php          // data.json の読み込みを実行
  │    ├ .htaccess
  │    └ index.php                // ルートファイル
  ├ css/
  ├ img/
  ├ js/
  │  └ app.min.js                 // jQuery AJAX で上述 corspan/index.php に data.json のデータを問い合わせる
  ├ index.html                    // ドキュメントルート。見えない要素に AJAX での問い合わせ先URLを記述
  └ etc.

肝の部分は以下。

  • サブディレクトリ corspan/ に PHP インターフェースプログラムとデータリソース(corspan/data/data.json)が存在
  • js/app.min.js から jQuery AJAX で corspan/index.php にデータの問い合わせをする
    • 問い合わせ先は自分のローカル環境ではなく、外部サーバを指定

検証

改修前・開発環境

まず CORS 関連のヘッダを出力しない、改修前の PHP へ開発環境から問い合わせた場合。

CORS 関連のヘッダを出力しない場合の開発環境からサーバへの問い合わせ。想定通り CORS のエラーで引っかかります
CORS 関連のヘッダを出力しない場合の開発環境からサーバへの問い合わせ。想定通り CORS のエラーで引っかかります

Access to XMLHttpRequest at ‘https://demo.example.jp/corspan/’ from origin ‘http://192.168.x.x:3000′ has been blocked by CORS policy: No ‘Access-Control-Allow-Origin’ header is present on the requested resource.

想定通り CORS のエラーで引っかかりました。

CORS 関連のヘッダを出力しない場合の開発環境からサーバへの問い合わせのリクエスト・レスポンスヘッダ
CORS 関連のヘッダを出力しない場合の開発環境からサーバへの問い合わせのリクエスト・レスポンスヘッダ

リクエストヘッダとレスポンスヘッダ。レスポンスヘッダに Access-Control-Allow-Origin 等がないことを確認しておきます。

改修前・サーバ環境

CORS 関連のヘッダを出力しない場合のサーバから自身への問い合わせ
CORS 関連のヘッダを出力しない場合のサーバから自身への問い合わせ

次にサーバに普通にアクセスした場合。この場合は同じオリジンなので CORS は発生せず、普通にデータが取得できます。

改修後・開発環境1

続いて、 CORS 関連のレスポンスヘッダを出力するように改修した後の開発環境からの問い合わせ。

CORS 関連のレスポンスヘッダを出力するように改修した後の開発環境からの問い合わせ。
CORS 関連のレスポンスヘッダを出力するように改修した後の開発環境からの問い合わせ。

取得できるようになりました。

CORS 関連のレスポンスヘッダ
CORS 関連のレスポンスヘッダ

リクエストヘッダとレスポンスヘッダ。レスポンスヘッダに Access-Control-Allow-OriginAccess-Control-Allow-Headers が出力されています。

Access-Control-Allow-Origin のホスト名はIPアドレスとなっています(画像では念のため隠していますが、http://192.168.x.x:3000 のような形で出力されています)。なお、テストのためポート番号は 3000 で決め打ちにしています(後述)。

CORS 関連のレスポンスヘッダ2
CORS 関連のレスポンスヘッダ2

続いて、URLバーで書き換えを行ってホスト名をIPアドレスから localhost にした場合。

Access-Control-Allow-Originlocalhost に変化しており、こちらも正しくデータが取得できていることが確認できました。

改修後・開発環境2

続いて、もう一つ異なる BrowserSync のセッションを起動します。今回はポート番号が 3000 ではなく 3002 になりました。

異なるポート番号の開発環境からサーバへの問い合わせ。想定通り CORS のエラーで引っかかります
異なるポート番号の開発環境からサーバへの問い合わせ。想定通り CORS のエラーで引っかかります

先程「ポート番号は 3000 で決め打ち」としたため、 3002 では Access-Control-Allow-Origin が出力されず、 CORS に引っかかりました。

改修前・サーバ環境

CORS 関連のヘッダを出力するよう改修した後のサーバから自身への問い合わせ
CORS 関連のヘッダを出力するよう改修した後のサーバから自身への問い合わせ

改修後でも、改修前と同様そもそも同じオリジンなので CORS は発生せず、以前と変わらずデータ取得ができています。

改修内容(要点)

以上より、検証のコードが動作することが確認できました。以下、コードです。

dist/corspan/src/config.php

<?php
return [
    // データパス
    'dataPath' => __DIR__ . '/../data/data.json',
    // 開発環境用: CORS回避ヘッダのためのドメイン設定
    'cors'     => [
        'http(s)?:\/\/localhost:3000',
        'http(s)?:\/\/192\.168\.([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.([1-9]?[0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5]):3000',
    ],
];

まずは設定部分。 cors キーに許可するオリジンの正規表現を配列形式で複数持たせることにしました。上記の場合、 localhost:3000192.168.x.x:3000 の2つの場合に対応する記述例となります。

dist/corspan/src/ControllerCors.php

<?php

namespace Corspan\Controller;

class CSCors
{
    protected $config;

    public function __construct()
    {
        $this->config = require(__DIR__ . '/config.php');
    }
    public function setHeaders($jsonArray)
    {
        $headers = [
            'HTTP/1.1 ' . $jsonArray['status'] . ' ' . $jsonArray['message'],
            'Content-Type: application/json; charset=utf-8',
        ];
        $origin = '';

        if(array_key_exists('Origin', getallheaders()) || array_key_exists('origin', getallheaders())) {
            // Cors の場合自動的にリクエストヘッダに `Origin` が追加される。それが getallheaders() で取得できるリクエストヘッダの中に存在する場合
            $requestOrigin = getallheaders()['Origin'];
            foreach($this->config['cors'] as $value) {
                // config の中にある cors の一覧と $requestOrigin を正規表現でチェック
                $regexStr = '/^' . $value . '$/i';
                if(preg_match($regexStr, $requestOrigin)) {
                    // マッチした場合 $origin に値を格納し、ループをブレーク
                    // $origin → レスポンスヘッダの Access-Control-Allow-Origin に記載する
                    $origin = $requestOrigin;
                    break;
                }
            }
        }
        if(mb_strlen($origin, 'UTF-8') > 0) {
            $headers = array_merge(
                $headers,
                [
                    'Access-Control-Allow-Origin: ' . $origin,
                    'Access-Control-Allow-Headers: GET, OPTIONS',
                ]
            );
        }

        return $headers;
    }
}

CORS 関連のヘッダを調整しているクラスがこちら。

初期状態は HTTP/1.1 200 OK. のようなHTTPステータスと Content-Type の2つのみがレスポンスヘッダとして出力する用の変数 $headers にセットされています。

そこに、以下の処理をします。

  • getallheaders() で取得したリクエストヘッダに Origin フィールドが存在するか
    • 存在する場合は上述 dist/corspan/src/config.phpcors キーの値と正規表現チェック
      • マッチした場合は Access-Control-Allow-Origin にそのオリジンを指定して $headers に追加(同時に Access-Control-Allow-Headers も指定)
      • マッチしない場合は上述の Access-Control-Allow-Origin, Access-Control-Allow-Headers の2つをセットしない

dist/corspan/src/ControllerJson.php

<?php

namespace Corspan\Controller;

class CSOutputJson
{
    protected $CSHelpers;
    protected $CSCors;

    public function __construct($CSHelpers, $CSCors)
    {
        $this->CSHelpers = $CSHelpers;
        $this->CSCors = $CSCors;
    }

    public function output($jsonArray)
    {
        $headers = $this->CSCors->setHeaders($jsonArray);
        foreach($headers as $value) {
            header($value);
        }

        echo $this->CSHelpers->jsonOutput($jsonArray);
        exit();
    }
}

レスポンスヘッダ・ボディの出力部分では上述 $headersheader() 関数で順番に出力しているだけです。


以上で簡単ではありますが PHP で Access-Control-Allow-Origin に複数オリジンを対応させたい場合の対応のサンプルとしてメモしておきます。

参考

CORS

複数オリジンへの対応

PHP

getallheaders

備考

このときにやった知識が活かされました……。

今回はリソースがサーバなのでこの方法(リソースサーバが自身)とは別ケース。

この記事を書いた人

アバター

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。