経緯
あるデータリソース(例: JSONファイル)に対して自作の PHP のインターフェースを作成し、 JS(jQuery) でデータの問い合わせを行う仕組みを作りました。
その後、よんどころない事情により開発環境(BrowserSync)からも本番のデータを取得したくなりました。そうすると、リクエストを投げるJSのオリジンは localhost:3000
や 192.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つのオリジンだけを指定することができます。サーバーが複数のオリジンからのクライアントに対応している場合、リクエストを行った特定のクライアントのオリジンを返さなければなりません。
……複数指定、できないのですね。
調査・実装
いくつかの記事を調べてみたところ、上述の通り「リクエストを行った特定のクライアントのオリジンを返」すという形になりそうです。
そこで、検証のため簡単なプロジェクトを作成しました。
ディレクトリ構造
生成された結果の構造を示します。
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 へ開発環境から問い合わせた場合。

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 のエラーで引っかかりました。

リクエストヘッダとレスポンスヘッダ。レスポンスヘッダに Access-Control-Allow-Origin
等がないことを確認しておきます。
改修前・サーバ環境

次にサーバに普通にアクセスした場合。この場合は同じオリジンなので CORS は発生せず、普通にデータが取得できます。
改修後・開発環境1
続いて、 CORS 関連のレスポンスヘッダを出力するように改修した後の開発環境からの問い合わせ。

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

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

続いて、URLバーで書き換えを行ってホスト名をIPアドレスから localhost
にした場合。
Access-Control-Allow-Origin
が localhost
に変化しており、こちらも正しくデータが取得できていることが確認できました。
改修後・開発環境2
続いて、もう一つ異なる BrowserSync のセッションを起動します。今回はポート番号が 3000
ではなく 3002
になりました。

先程「ポート番号は 3000
で決め打ち」としたため、 3002
では Access-Control-Allow-Origin
が出力されず、 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:3000
と 192.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.php
のcors
キーの値と正規表現チェック- マッチした場合は
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();
}
}
レスポンスヘッダ・ボディの出力部分では上述 $headers
を header()
関数で順番に出力しているだけです。
以上で簡単ではありますが PHP で Access-Control-Allow-Origin
に複数オリジンを対応させたい場合の対応のサンプルとしてメモしておきます。
参考
CORS
複数オリジンへの対応
- 複数のWebサイトに対してクロスドメイン通信を許可するCORS設定方法 | Developers.IO
- Access-Control-Allow-Originヘッダで複数のオリジンドメインを許可する方法 – ぷれすとぶろぐ
PHP
getallheaders
備考
このときにやった知識が活かされました……。
今回はリソースがサーバなのでこの方法(リソースサーバが自身)とは別ケース。