ReactからLaravelのAPIサーバーを叩く + CORS概説

## 初めに

以前、[LaravelをAPIサーバーとして利用する記事](https://qiita.com/10mi8o/items/d438286313f91be17304)を書いたのですが、その続きとして、ReactとLaravelを使って、CORSとは何かを解説しながら、実際にLaravel側に設定を書くところまでをやってみたいと思います。

※ Laravelでは、CORSの設定は、[fruitcake/laravel-cors](https://github.com/fruitcake/laravel-cors)を使うことが一般的かなと思いますが、本記事では、Middlewareの実装も行っています。

## CORSって何？

Cross-Origin Resource Sharingの略で、`コルス`と呼びます。日本語訳すると`オリジン間リソース共有`と呼びます。この設定をしていないと、フロントからAPIを叩いて、APIサーバーの値を取得したり、保存したりということが出来ません。

>Cross-Origin Resource Sharing (CORS) は、追加の HTTP ヘッダーを使用して、あるオリジンで動作しているウェブアプリケーションに、異なるオリジンにある選択されたリソースへのアクセス権を与えるようブラウザーに指示するための仕組みです。

[オリジン間リソース共有 (CORS)](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS)

## Cross-OriginのOriginってそもそも何？

Originとは、URL中の `スキーム` `ホスト` `ポート`の組み合わせのことです。

例：https://google.com:443

[Origin(オリジン)](https://developer.mozilla.org/ja/docs/Glossary/Origin)

ブラウザはセキュリティ上の理由で、スクリプトによって開始されるHTTPリクエストを制限しています。これは[「同一オリジンポリシー」](https://developer.mozilla.org/ja/docs/Web/Security/Same-origin_policy)に従う事とイコールで、APIを叩く際に、CORSの設定をしていない場合、**同じオリジンに対してのみしかリクエストを行うことができません。**これは以下のような脆弱性を防ぐことを目的としています。

#### CSRF(Cross-Site RequestForgeries)
直訳のまま、サイト横断的にリクエストを偽装する攻撃で、Web アプリケーションのユーザーが、意図しない処理をWebサーバー上で実行される脆弱性です。

本来はログインしたユーザーしか実行できない記事の書き込みを勝手にされたり、登録情報を強制的に変更されたりしてしまいます。

#### XSS(Cross Site Scripting)
ユーザーが Web サイトにアクセスすることで不正なスクリプトがWebブラウザ(Client)で実行されてしまう脆弱性です。

利用者のCookieが盗まれ、Cookie内にある利用者のセッション情報がそのまま使用されてしまう「なりすまし」の危険性があります。

繰り返しになりますが、上記の脆弱性を回避するために、APIを叩く際には、同一オリジンポリシーに従って、通常は異なるオリジンに対してリクエストを行うことができません。

## CORSって何のためにあるの？
WebクライアントとAPIサーバーで分けて運用する場合など、異なるオリジンの間で通信を行う場合には、CORSの設定が必要です。

CORSは、ブラウザから情報を読み取ることを許可するオリジンを設定することを可能にします。

今回のケースでいうと、Reactアプリケーションから、Laravelで作ったAPIサーバーにリクエストを投げる際に、CORSの設定を行っていないと、エラーが返ってきてしまいます。

## CORSの二つのリクエスト
CORSには、リクエストの種類として、`単純リクエスト(Simple Request)`と`プリフライトリクエスト(Preflight Request)`に分類されています。

### 単純リクエスト(Simple Request)

以下のメソッドが単純リクエストで許可されています。

- GET
- POST
- HEAD

詳細は以下を参照してみてください。
[**単純リクエスト**](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Simple_requests)

### プリフライトリクエスト(Preflight Request)

HTTPメソッドには、サーバー情報に副作用を引き起こすメソッド（MIMEタイプを伴うPOSTやDELETE等）がありますよね。この場合、サーバーから対応するメソッドの一覧を取得し、サーバーの「認可」に基づいてリクエストを送信する必要があります。

方法としては、リクエストの始めに`OPTIONS`メソッドによるHTTPリクエストを他のドメインにあるリソースに向けて送り、実際のリクエストを送信しても安全かどうか確かめます。（＝***アクセスを許可するメソッドをレスポンスヘッダーに含める必要があります***）

以下のメソッドが、プリフライトリクエストでは許可されています。

- PUT
- PATCH
- DELETE
- CONNECT
- OPTIONS
- TRACE

詳細は以下を参照してみてください。
[**プリフライトリクエスト**](https://developer.mozilla.org/ja/docs/Web/HTTP/CORS#Preflighted_requests)

## CORSの設定がない状態でReactからAPIサーバーを叩いてみる

以下コンポーネントは、マウントされた時にAPIサーバーから値を取得する処理と、入力した値をPOSTして保存する処理が書かれたコンポーネントになります。

実際に手元で試したい場合は、以下記事を参考にAPIサーバーも立てて試してみてください。
[LaravelでサクッとAPIサーバーを立てる](https://qiita.com/10mi8o/items/d438286313f91be17304)

```react:検証用コンポーネント
export default function Component(props) {

  const headers = {
    "Content-type": "application/json",
  }

  const [articles, setArticles] = useState([]);
  const [newArticle, setNewArticle] = useState({title: "", body: ""});

  useEffect(()=> {
    const get = async () => {
      const res = await axios("http://127.0.0.1:8000/api/articles");
      setArticles(res.data)
    }
    get();
  },[])

  const handleOnCreateNewArticle = async () => {
    const data = {
      title: newArticle.title,
      body: newArticle.body
    }

    await axios.post("http://127.0.0.1:8000/api/articles", data, {
      headers
    })
    
    const res = await axios("http://127.0.0.1:8000/api/articles");
    setArticles(res.data)
  }

  return (
    <>
      <h1>
        {props.title}
      </h1>
      <h2>ArticleList</h2>
      {articles.map((article) => 
        <div key={article.id}>
        <div>TITLE:{article.title}</div>
        <div>BODY:{article.body}</div>
        <hr />
        </div>
        )}

      <input type="text" name="title" onChange={(e)=>{setNewArticle({...newArticle, title: e.target.value})}} value={newArticle.title} placeholder="New Title?"/>
      <input type="text" name="body" onChange={(e)=>{setNewArticle({...newArticle, body: e.target.value})}} value={newArticle.body} placeholder="New Body?"/>
      <button onClick={ handleOnCreateNewArticle }>Create</button>

    </>
  )
}
```

実際にReactと、LaravelのAPIのローカルサーバーを起動して、検証してみます。

React：`http://localhost:3000`
↓リクエスト
Laravel(APIサーバー)：`http://127.0.0.1:8000/api/articles`

すると、以下メッセージがDevToolで確認できるはずです。
「Access-Control-Allow-Originヘッダーが存在しないので、CORSポリシーによってブロックされていますよ」といったメッセージが表示されていますね。

```
Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/articles' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```

続いて、新規Articleを作成するためにPOSTメソッドでデータを送信してみます。すると、以下エラーが表示されます。プリフライトリクエストのチェックにパスしてませんと表示されています。

```
Access to XMLHttpRequest at 'http://127.0.0.1:8000/api/articles' from origin 'http://localhost:3000' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.
```
以上で、CORSの設定をしていないとAPIを叩いた際にエラーとなることがわかりました。それでは、`Access-Control-Allow-Origin`レスポンスヘッダーを追記して、CORSの設定をしていきます。

## LaravelでCORS設定を行う

### Middlewareを作成する

設定は簡単です。まずミドルウェアを作成しましょう。

```terminal
php artisan make:middleware Cors
```

```php:app/Http/Middleware/Cors.php
class Cors
{
    /**
     * Handle an incoming request.
     *
     * @param  \Illuminate\Http\Request  $request
     * @param  \Closure  $next
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        return $next($request)
        ->header('Access-Control-Allow-Origin', 'http://localhost:3000')
        ->header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
        ->header('Access-Control-Allow-Headers', 'Content-Type');
    }
}
```

ここで、レスポンスのHTTPヘッダーに３つのフィールドを追加しています。簡単に各フィールドの役割について見ていきます。

### Access-Control-Allow-Origin

[MDN：Access-Control-Allow-Origin](https://developer.mozilla.org/ja/docs/Web/HTTP/Headers/Access-Control-Allow-Origin)
>Access-Control-Allow-Origin レスポンスヘッダーは、指定されたオリジンからのリクエストを行うコードでレスポンスが共有できるかどうかを示します。

MDNの記述の通りですが、APIサーバー(ここではLaravel)へのアクセスを許可するオリジン(ここではReact)を指定しています。
上記の例だと、`http://localhost:3000`は許可していますが、それ以外は、同一オリジンポリシーに従って、アクセス不可です、という設定になっています。

### Access-Control-Allow-Methods
許可するHTTPメソッドを指定します。
Preflightリクエストを送信する場合には、ここに許可するメソッドを追記します。上記の例では、`GET` `POST` `PUT` `DELETE` `OPTIONS`を追記しています。

### Access-Control-Allow-Headers
許可するHTTPヘッダーを指定します。
上記の例では、`Content-Type`ヘッダーが指定される値が設定されます。

## 作成したミドルウェアをカーネルに追加

続いて、作成したミドルウェアをカーネルに追加しましょう。

```php:app/Http/Kernel.php
    protected $routeMiddleware = [
        'auth' => \App\Http\Middleware\Authenticate::class,
        'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
        'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
        'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
        'can' => \Illuminate\Auth\Middleware\Authorize::class,
        'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
        'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
        'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
        'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
        'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
        'cors' => \App\Http\Middleware\Cors::class,
    ];
```

## ルーティング

最後にルーティングを以下のように書き換えます。

```php:routes/api.php
Route::group(['middleware' => ['api', 'cors']], function(){
    Route::options('articles', function() {
        return response()->json();
    });
    Route::resource('articles', 'Api\ArticlesController');
});
```

## CORS設定後に実際にAPIを叩いてみる
以上で、CORSの設定は完了です。
先ほど、設定した３つのフィールドもヘッダーに含まれていることが確認できます。
![スクリーンショット 2020-11-28 19.08.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/32d3a3b7-e018-ec1a-8aab-6983c5e996b9.png)

実際にArticlesのデータが返ってきていることも確認できました。
![スクリーンショット 2020-11-28 19.08.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33632/ece3a4b1-3296-3061-e25e-a03b320bf2c2.png)

以上で、CORSの設定からAPIを叩くところまで出来るようになると思います。