【Vue ✕ 天気予報API③】CORS未対応APIにプロキシサーバで対応

# はじめに

[天気予報API]()からリソースを取得し、一週間分の天気を表示する簡単なWEBアプリケーションをVueで作成し、GitHub Pagesで公開しました。

APIなどの外部リソースをCORSで取得するために、PROXY（サーバ）で対応しました。そこでの対処法を共有いたします。バックエンドの処理を使用できる環境でしたが、このような記事が少なかったので、あえてプロキシを使う方法にしました。


# 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](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy)では、オリジンについてこのように説明されています。

> 以下の表は、各行の 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: 'ココ'` に指定します。

```vue.config.js
module.exports = {
    configureWebpack: {
        devServer: {
            proxy: {
                '/api': {
                    target: 'https://www.metaweather.com',
                }
            }
        }
    }
}
```
このようにリクエストします。

```vue.js
 axios.get("/api/location/1118108")
```


### 2. 拡張機能

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

[Allow CORS](https://chrome.google.com/webstore/detail/allow-cors-access-control/lhobafahddgcelffkeicbaginigeejlf)をクリックし、「Chromeに追加」
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681029/4b847978-9a5b-acac-79ca-8e22d416b6de.png">

Chromeの画面右上から、追加した拡張機能をクリック。画像のようにアイコンのCがオレンジ色になったらOKです。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681029/71008047-3f7d-a7dd-133e-83150ea705b9.png">

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


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

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

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


## プロキシサーバとは

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

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

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

[Using React hooks to handle API calls](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](https://github.com/Rob--W/cors-anywhere) プロジェクトを作成してください。

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

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

 [Heroku スターターガイド (Node.js)](https://devcenter.heroku.com/ja/articles/getting-started-with-nodejs) これが公式のドキュメントです。かなりまとまっているので、流れのまま進めるだけで大丈夫です。
データベース設定など、必要ない項目はスルーしてください。

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

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

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

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


### 4. heroku環境変数を設定

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

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

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

```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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681029/cc4d74e5-5593-408f-2d5f-febc605aa71b.png)



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

![dev-tool.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/681029/e02633d2-cf98-7cc5-88d9-79c8a2ae9a51.png)

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

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

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

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


# まとめ

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



# 参考文献

- [Using React hooks to handle API calls](https://medium.com/@deepak13245/using-react-hooks-to-handle-api-calls-d6bb4ae91188)
- [cors-anywhere（GitHub）](https://github.com/Rob--W/cors-anywhere)
- [オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)
- [Heroku スターターガイド (Node.js)](https://devcenter.heroku.com/ja/articles/getting-started-with-nodejs)
- [DevServer | webpack](https://webpack.js.org/configuration/dev-server/)