Angular2/Angular4のHTTPでPOSTリクエストのCORS問題を解決する

Google先生がなかなか教えてくれなかったので、穴埋め係させていただきます！
※Angular2/4 の前身の某JSのことを A......JS と伏せ字にしていますが、検索キーワードに引っかかってほしくないためです。別に裏の意図はありません……。

# 何がしたい？

Angular経験１ヶ月程度の初心者さんなのですが、ふつうにhttpモジュールでPOSTリクエストをしようとして、CORSで壮大にコケたというお話です。OPTIONSという謎のリクエストが走ったり。どうやって突破するのか？

# CORS(Cross-Origin Resource Sharing)とは？
![プレゼンテーション1.jpg](https://qiita-image-store.s3.amazonaws.com/0/129854/9be2d6e8-ebab-1505-4200-f69ee249e1b7.jpeg)

今見ているサイトとは違うサイトから、情報を取ってきたり渡したりしたらダメだよ、というブラウザのセキュリティ対策です。詳しくはこちらとか。→ [HTTP アクセス制御 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control)
そもそも、フロントとバックが別のサイトを参照している、なんて不自然な状況かもしれませんが、開発段階で、フロントはローカル環境、バックは本サーバ（テストサーバ）という状況はよくあるんじゃないかと思うんです。

# 結論 ―― とりあえず解決する方法

理屈だとか、高度なセキュリティだとかは置いといて、とにかく動くようにしたい場合の結論ファースト。

## フロント側 Angular の書き方

結局は、A......JS のドキュメントでも言われているとおり、リクエストヘッダに1文加える方法がカンタンです。Augular2/Angular4では、下記のようにします。

```ts:sample.component.ts
import { Component, OnInit } from '@angular/core';
import 'rxjs/add/operator/map'
import { Http, Headers } from "@angular/http"; // Http に加えて Headers をインポート

@Component({
  selector: 'app-sample',
  templateUrl: './sample.component.html',
  styleUrls: ['./sample.component.scss']
})
export class SampleComponent implements OnInit {

  headers :Headers;

  constructor( private http: Http ) {  
  }

  ngOnInit() {
    // POST で使うヘッダーを作っておく
    this.headers = new Headers();
    this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
  }

  // 何かしらHTTPリクエストを走らせるイベントなど
  onSampleSubmitted( postParam :Array )
  {
    this.http.post('https://myhost.com/api/', postParam, {headers: headers} ) // ここでheadersを追加
             .subscribe( response => console.log(response.json()) );
  }
}
```

## サーバー側の書き方

CORS問題はフロント側だけは解決できません……。

### htaccess に書く

APIのエンドポイント（上記で言うと /api ディレクトリ ）に、下記を設置してしまうのがカンタンです。言語を選ばないし。

```apache:.htaccess
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin *
</IfModule>
```

### PHPでJSONを受け取る

PHPの場合は、これにもうひと工夫必要です。Angularから送られてくるPOST情報は、PHPが理解できるForm形式（ key=value1&key2=value2 というURLパラメータの形式）ではなく、**JSON**です。なので、いつもの $_POST で受け取ろうとすると空っぽ。そこで次のようにする必要があります。

```php:ExampleAPIController.php
    switch( $_SERVER["REQUEST_METHOD"] ){
      case 'POST':
        $json_string = file_get_contents('php://input');
        $posted_json = json_decode($json_string,true); // これを $_POST代わりに使う
        break;
    }
```
**2017.10.15 加筆** json_decode は第２引数を true にして配列に。 $_POST と同じように使えるので。

# 解説 ―― もう少しちゃんとした解決策

上記解決策はいくつか「キモチワルイ」ところがあります。それをちゃんと解決するために、実際にフロントとサーバーとどういうやり取りをしているのか、詳しく見ながら検証します。

## Allow-Origin * 問題

せっかくCORSのために強力なセキュリティが用意されているのに、そのドアを「全開」にしておくのは褒められたものではありません。Angularであれば、基本的にフロントは特定のサーバー（この場合はローカルホスト）しかないので、Allow-Originもそのサーバーに限定します。
（localhostに限定って、localhostは誰でも建てられるからセキュリティとして全然用をなしてないという声はスルーします……）

問題はどうやってそれを書くか？ サブドメインは？ http は要るの要らないの？

実は難しく考える必要はありません。先の .htaccess を一旦削除してAPIアクセスをして、ブラウザのコンソールを見てください。

![名称未設定-2.jpg](https://qiita-image-store.s3.amazonaws.com/0/129854/254c189f-b191-493e-09b8-8be5f39fe79e.jpeg)

親切に「Access-Control-Allow-Origin で http://localhost:4200 が許可されていません」と表示されています。それをそのまま書きます。

```apache:.htaccess
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
</IfModule>
```

httpだけでなく、ポート番号まで要るとは思いませんでした……。よく考えたら、これはブラウザが単純に文字列判定するためのもので、サーバー側からアクセスできるかどうかは関係ない、ということなのかもしれません。


## Content-Type: form-urlencoded 問題

そもそもなぜ application/x-www-form-urlencoded にするのか？というと、こうしておくと、ブラウザがCORSのための「プリフライトリクエスト」を行わないため、とあります。わかりやすく言うと、CORSのセキュリティを1つ省略する＝**ちょっとセキュリティが弱くなる**ということ。
もう1つ問題があって、リクエストヘッダで「コンテンツはFORMだ」と言ってるのに実際はJSONと、**宣言と中身が違う**こと。PHPなどの受け取り側はあまり気にしてないので違ってても支障はありませんが、礼儀として、中身がJSONなら Content-Type も application/json としたいところです。

下記が、別のツールを使って正しい form-urlencoded と json のリクエストを作ってみたところ。宣言と中身が一致しているリクエストです。

```:いつものブラウザが送っているリクエスト
content-length: 21
content-type: application/x-www-form-urlencoded # FORM
key1=value1&key2=value2                         # FORM
```

```:上記プログラムでAngularが送ろうとするリクエスト
content-length: 33
content-type: application/x-www-form-urlencoded # FORMなのに
{"key1":"value1","key2":"value2"}               # JSON
```

```:こうしたい
content-length: 33
content-type: application/json    # JSON!
{"key1":"value1","key2":"value2"} # JSON!!
```

### プリフライトリクエスト OPTIONS に応答する

結論を先に言うと、難しいことを考える必要はなくて、.htaccess に必要なことを書いてあげるだけです。

```
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
    Header set Access-Control-Allow-Headers content-type
    Header set Access-Control-Allow-Methods GET, POST, PUT, DELETE
</IfModule>
```

### Angular を修正

サーバーの準備できたら、Angularも修正します。
下記のようにすると、OPTIONSのプリフライトリクエストが走るようになりますが、サーバーからちゃんと許可が降りると、その後すぐにPOSTが走ります。

```ts:sample.component.ts

  ngOnInit() {
    this.headers = new Headers();
    // this.headers.append('Content-Type', 'application/x-www-form-urlencoded');
    this.headers.append('Content-Type', 'application/json');
  }

```


### プリフライトリクエスト OPTIONS とは？

プリフライトリクエストとは、ブラウザが、CORSにあたるとき、別サーバーに対して何が許可されているかを実際のリクエストをする前に確認するリクエストで、実際にPOSTの前にOPTIONSというリクエストを飛ばします。
このOPTIONSというリクエストに対して、Allow-Methods として POST が返ってきたら、本番の POST をリクエストするという仕組みです。

![プレゼンテーション1.png](https://qiita-image-store.s3.amazonaws.com/0/129854/84078e5a-e6b8-9756-7cc7-483508403c9e.png)


### 補足 やらなくていいこと

#### OPTIONS を含める

~~許可メソッドに OPTIONS を含める必要はありません。~~
~~OPTIONS 以外に何が許可されているかを問い合わせるのが OPTIONS なので、OPTIONS がなくても動きます。~~

```apache:こうする必要はない
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
    Header set Access-Control-Allow-Headers content-type
    Header set Access-Control-Allow-Methods GET, POST, PUT, DELETE, OPTIONS
</IfModule>
```

と書いたところで……。どうも Allow-Methods 自体も必要ないようです。これは、上の図のように、サーバーが Allow-Methodsを返してフロントで判断するのではなく、その前にサーバーが Request-Methods を見て許可するか判断しろ、ということなのかも。

```apache:これでも動く
<IfModule mod_headers.c>
    Header set Access-Control-Allow-Origin http://localhost:4200
    Header set Access-Control-Allow-Headers content-type
</IfModule>
```

#### OPTIONSのリクエストに明示的に応える

例えばPHPのプログラム内で、MethodがOPTIONSの場合の処理を用意する必要はありません。.htaccessの応答だけで十分。仮に何かを出力しても、OPTIONSの応答では、ブラウザは本文を無視します。
我が家では下記のように、なにもしないことを明記しています。

```php:ExampleAPIController.php
    switch( $_SERVER["REQUEST_METHOD"] ){
      case 'POST':
        $json_string = file_get_contents('php://input');
        $posted_json = json_decode($json_string); // これを $_POST代わりに使う
        break;
            
      case 'OPTIONS':
        exit;
    }
```

# 感想

というわけで、AngularでPOSTしたかっただけなのにどっぷりハマって、HTTPのリクエストと応答の仕組みまでいろいろ勉強させていただきました。FORM形式とJSONの違いとか、JSONだとPHPは認識しないなど、知らなかったこともわかったし、クライアントとサーバー間のやりとりを、もう少しディープに書くことができるようになった、気がします。