⚙️

CORS非対応APIに、PROXYを中継して通信を行う

2021/02/27に公開

はじめに

APIなどの外部リソースをCORSで取得するために、PROXY(サーバ)で対応しました。その時学んだブラウザ周りの規約や、対処法がメインの内容となっています。
(metaweatherという天気予報APIを利用したときの備忘録です)

バックエンドの処理を使用できる環境でしたが、このような記事が少なかったので、あえてプロキシを使う方法にしました。

localhostからAPIにリクエスト

すると、下記のようなエラーが発生しました。

Access to XMLHttpRequest at 'https://www.metaweather.com/api/location/1118108' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

「Access-Control-Allow-Originがリクエストのhttpヘッダに無いことが理由で、CORSの制約に引っかかってます」とのエラーが発生。

同一オリジンポリシーの制御対象であることが理由です。
さらに調べてみると、今回使用したAPIがJSONPにもCORSにも対応しておらず、ブラウザからの呼び出しが不可能であることが判明しました。

Same-Origin Policy

  • 同一オリジンポリシー
  • SOP

SOPは、異なるオリジンへの干渉をブラウザレベルで防ぐことにより、ユーザにとってのセキュリティを担保するための仕組みです。
このおかげで、XSS, CSRFなどの脆弱性を防ぐことが出来ます。

今回制作した天気予報アプリで扱っているAjaxは、XMLHttpRequest(サーバーから非同期にデータを取得する仕組み)を使っており、これがSOPの制約の対象となっています。

CORS(コルス)

  • オリジン間リソース共有
  • Cross Origin Resource Sharing

SOPの制約に対し、「異なるオリジンからデータを送受信したい」ときのためにCORSが存在します。

CORSとは、SOPを例外的に無効化し、安全に情報の受け渡しをおこなうためのブラウザの仕組みのことです。この仕組みのおかげで、違うオリジン同士でも通信ができます。
chromeやsafariなど一般的に使用されるブラウザは、ほぼCORSをサポートしています。

オリジン

オリジンはスキーム・ホスト・ポート番号のセットのことで、すべてが同じであれば同一オリジン、1つでも違ったら別のオリジン(クロスオリジン)となります。

https://www.example.com:80/login

上記URLを例に挙げた場合は以下のようになります。
スキーム(プロトコル) = https
ホスト = www.example.com
ポート = 80

ホストが違うので別オリジン

今回の場合、Github pagesで公開したサイトのURLは
https://sohh85.github.io/weather-app/
天気予報APIは
https://www.metaweather.com
このように、ホストが違うため別オリジンとなります。

同一オリジンの具体例

MDNでは、オリジンについてこのように説明されています。

以下の表は、各行の URL が http://store.company.com/dir/page.html と同じオリジンであるかを比較したものです。

| URL | 結果 | 理由 |
|:-----------------|:------------------|:------------------|
|http://store.company.com/dir2/other.html| 同一オリジン | パスだけが異なる |
|http://store.company.com/dir/inner/another.html| 同一オリジン | パスだけが異なる |
|https://store.company.com/page.html| 不一致 | プロトコルが異なる |
|http://store.company.com:81/dir/page.html| 不一致 | ポート番号が異なる (http:// は既定で80番ポート) |
|http://news.company.com/dir/page.html| 不一致 | ホストが異なる |

開発環境での対処

webpackの便利機能である DevServer を使う方法と、拡張機能を使う方法の2種類を紹介します。

1. webpack-dev-serverのプロキシ機能

設定ファイルに下記を追加してください

任意のパス /api を別のサーバである https://www.metaweather.com にプロキシさせることができます。フォワードするURLを target: 'ココ' に指定します。

module.exports = {
    configureWebpack: {
        devServer: {
            proxy: {
                '/api': {
                    target: 'https://www.metaweather.com',
                }
            }
        }
    }
}

このようにリクエストします。

 axios.get("/api/location/1118108")

2. 拡張機能

chromeの拡張機能を使うことで開発環境ではAPIリクエストを行うことができました。

Allow CORSをクリックし、「Chromeに追加」

Chromeの画面右上から、追加した拡張機能をクリック。画像のようにアイコンのCがオレンジ色になったらOKです。

これでもう一度通信を行うと、CORSでブロックされずリソースが取得できます。

本番環境ではプロキシサーバで対応

今回使用したAPIである MetaWeather はCORS未対応です。通信を行うためには、バックエンドのプログラムを中継し、APIリクエストを行う必要があります。

そのため、今回はherokuにNode.jsで中継サーバを建てて対応することにしました。

プロキシサーバとは

「プロキシ」とは日本語で「代理」という意味であり、クライアントとサーバの間に中継役として配置するサーバのことです。プロキシは、クライアントから見た場合、サーバのように振る舞い、サーバから見た場合、クライアントのように振る舞います。

悪意のあるプロキシサーバにIDやパスワードなどを抜き取られてしまう可能性があるので、無料で提供されている公開プロキシサーバーを使用する際は、注意が必要です。また、プロキシサーバを挟んだことで通信が遅くなってしまう、などのデメリットがあります。

リソース取得のために実施したこと

https://medium.com/@deepak13245/using-react-hooks-to-handle-api-calls-d6bb4ae91188

この記事を参照し、PROXY(サーバー)で対応しました。2021年2月現在では、下記の手順で、問題なく完了できました。

1. cors-anywhereを git clone

2. Herokuのスターターガイドを参照しデプロイ

3. axios.getメソッド内のURLを変更

4. Heroku環境変数を設定し、アクセスを許可するオリジンを指定

1. cors-anywhere を clone

下記コマンドを実行し、cors-anywhere プロジェクトを作成してください。

git clone https://github.com/Rob--W/cors-anywhere.git

2. スターターガイドを参照しデプロイ

https://devcenter.heroku.com/ja/articles/getting-started-with-nodejs

公式のドキュメントを参考にします。かなりまとまっているので、流れのまま進めるだけで大丈夫です。
データベース設定など、必要ない項目はスルーしてください。

3. axios.getメソッドのURLを変更

プロキシサーバのURL + リソース取得のためのURL のように記述してください。
こうすることで、このサイトとはドメインが異なるAPIにフォワードされます。

https://cors-anywhere.herokuapp.com がプロキシサーバのURLだとしたら、このようにリクエストします。

 axios.get("https://cors-anywhere.herokuapp.com/https://www.metaweather.com/api/location/1118108")

4. heroku環境変数を設定

下記をキー名、値にオリジンを指定し、herokuに環境変数を登録します。これにより、不正なアクセス・使用を防ぐことができるので極力設定してください。
細かいオプションなどは、cors-anywhere の Read.me をご確認ください。

CORSANYWHERE_WHITELIST → アクセスを許可
CORSANYWHERE_BLACKLIST → アクセスを拒否

herokuで設定した環境変数が、プロキシサーバのserver.jsに記述された、下記ソースコードで読み込まれます。

var originBlacklist = parseEnvList(process.env.CORSANYWHERE_BLACKLIST);
var originWhitelist = parseEnvList(process.env.CORSANYWHERE_WHITELIST);
function parseEnvList(env) {
  if (!env) {
    return [];
  }
  return env.split(',');
}

環境変数を設定できているかは、ターミナルでheroku configコマンドを実行して確認してください。

chromeデベロッパーツールで確認

Network → Name の中から表示したい項目を選択 → Preview の順にクリックすると、画像のようにAJAX通信でAPIから取得した値を確認できます。
画像のように実際に値が取れていたら、プロキシを使ったAJAX通信が成功しています。

preview.png

Network → Name の中から表示したい項目を選択 → Headers の順にクリックすると、画像のようにリクエスト・レスポンスヘッダーを確認できます。

dev-tool.png

レスポンスヘッダー内に Access-Control-Allow-Origin: * とあり、全てのオリジンがアクセスを許可されているのがわかります。

もし下記がレスポンスヘッダーに付与された場合

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

http://example.comから?だったらリソースあげるよ」
という判断をサーバ側ですることができ、CORSにより安全な通信を行います。

まとめ

以上になります。
CORSや同一オリジンポリシーなど、今まで理解できていない部分を学びつつアウトプットしてみました。
この記事が、だれかの役に立てば幸いです。

参考文献

Discussion