file_get_contentsでCakePHP3.6にPOST送信する

# CakePHP3.6でCSRFを適用するルーティングを分ける

CakePHPをAPIサーバーとして、フロントエンド側からfile_get_contents()でPOST送信をしてデータを受け取ります。
ほかの記事にもあるとおり、Cake3.6ではMiddlewareでCSRFが有効になっているので、POST送信ではエラーになってしまいます。

全体でCSRFの適用を無効にしてしまえばいいのですが、投稿フォームなど既に実装済みのものにはそのまま有効にして、新しく実装するデータを返すだけの機能には無効にしようと思いました。

結論としては、全体のCSRFを無効にして、デフォルトのルーティングで有効にし、新しく実装する機能のルーティングでは無効にします。
そのため、CSRFを適用しない機能は、apiというプレフィックスを切るようにしました。

## エラーの発生状況

CakePHPのコントローラーはこのような感じです

```php:
    /**
     * 配列[10, 20, 30]をJSONで返すだけのコントローラー
     */
    public function responseJsonData()
    {
        $this->autoRender = false;
        $this->viewBuilder()->setLayout(false);

        $this->request->accepts('application/json');
        $request_data = $this->request->getData();

        $result = [10, 20, 30];

        $this->response->getCharset('UTF-8');
        $this->response->getType('json');
        $this->response->getHeaders('Access-Control-Allow-Origin: *');
        $this->response->getHeaders('Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept');

        echo json_encode($result, JSON_UNESCAPED_UNICODE);

    }
```

送信側はこんな感じのスクリプトです。
POSTでfile_get_contents()するだけなのですが、CSRFのエラーになりました。

```php
<?php

$headers = [
    "User-Agent: Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1)",
    "Accept-Language: ja",
    "Cookie: test=hoge",
    "Content-Type: application/x-www-form-urlencoded",
];

$data = [
    'title' => '送信テスト',
    'body' => 'テスト',
];

$opts = [
    'http' => [
        'method' => 'POST',
        'header' => implode("\r\n", $headers),
        'content' => http_build_query($data),
        'ignore_errors' => true,
    ],
];

$ctx = stream_context_create($opts);

$result = file_get_contents('http://example.com/caketest/api/WebApi/response_json_data', false, $ctx);

var_dump($result);
var_dump($http_response_header);

```

CSRFが適用されているときに出るエラーです。403でした。

```php:
Warning: file_get_contents(http://example.com/caketest/WebApi/response_json_data): failed to open stream: HTTP request failed! HTTP/1.1 403 Forbidden in /var/www/html/caketest/webroot/WebApi/by_file_get_contents_post.php on line 30

```

## CSRFの適用をルーティングで分ける

### Middlewareに適用されているCSRFをいったん無効にする
CsrfProtectionの追加をコメントアウトする

```php:src/Application.php
    public function middleware($middlewareQueue)
    {
        （中略）

            // Add routing middleware.
            // Routes collection cache enabled by default, to disable route caching
            // pass null as cacheConfig, example: `new RoutingMiddleware($this)`
            // you might want to disable this cache in case your routing is extremely simple
            ->add(new RoutingMiddleware($this, '_cake_routes_')); << カンマを足す

            （以下、コメントアウト）
            // Add csrf middleware.
            // ->add(new CsrfProtectionMiddleware([
            //     'httpOnly' => true
            // ]));

        return $middlewareQueue;
    }
```
### デフォルトのルーティングにCSRFを適用する
デフォルトのルーティングにCSRFのミドルウェアを登録して適用します。
これはCakePHP3.8のデフォルトのルーティングに記載されているものを持ってきました。

```php:config/routes.php
Router::scope('/', function (RouteBuilder $routes) {
    // Register scoped middleware for in scopes.
    $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([
        'httpOnly' => true
    ]));

    /**
     * Apply a middleware to the current route scope.
     * Requires middleware to be registered via `Application::routes()` with `registerMiddleware()`
     */
    $routes->applyMiddleware('csrf');

    （以下略）

});

```
### プリフィックスapiのルーティングを追加
デフォルトのルーティングとは別に、プリフィックスapi用のルーティングを追加しました。こちらにはCSRFは適用されません。

```php:config/routes.php
Router::scope('/', function (RouteBuilder $routes) {
    
    （略）

});

Router::prefix('api', function ($routes) {
    $routes->connect('/', ['controller' => 'Pages', 'action' => 'index']);
    $routes->fallbacks(DashedRoute::class);
});
```


## おわり
データを返すだけのAPIであればいいかもしれないですが、書き換えなどが発生する場合はCSRFのほか対策が必要になるかと思います。今回はルーティングによってCSRFの適用を分けるTipということで…


## 参照リンク
[cakephp3 api実装にてcsrfを無効にする | なんちゃってプログラマのただのメモ](http://blog.ryusuke-sano.com/it/php/cakephp/cakephp3-api%E5%AE%9F%E8%A3%85%E3%81%AB%E3%81%A6csrf%E3%82%92%E7%84%A1%E5%8A%B9%E3%81%AB%E3%81%99%E3%82%8B/)

## 参考 : Cake3.8とCake3.6のmiddleware()

CakePHP3.8は、middleware()でCSRFの適用がされていない

```php:src/Application.php
public function middleware($middlewareQueue)
    {
        $middlewareQueue
            // Catch any exceptions in the lower layers,
            // and make an error page/response
            ->add(new ErrorHandlerMiddleware(null, Configure::read('Error')))

            // Handle plugin/theme assets like CakePHP normally does.
            ->add(new AssetMiddleware([
                'cacheTime' => Configure::read('Asset.cacheTime')
            ]))

            // Add routing middleware.
            // If you have a large number of routes connected, turning on routes
            // caching in production could improve performance. For that when
            // creating the middleware instance specify the cache config name by
            // using it's second constructor argument:
            // `new RoutingMiddleware($this, '_cake_routes_')`
            ->add(new RoutingMiddleware($this));

        return $middlewareQueue;
    }
```

CakePHP3.6のmiddleware()は、初期でCSRFの適用がされている

```php:src/Application.php
    public function middleware($middlewareQueue)
    {
        $middlewareQueue
            // Catch any exceptions in the lower layers,
            // and make an error page/response
            ->add(ErrorHandlerMiddleware::class)

            // Handle plugin/theme assets like CakePHP normally does.
            ->add(new AssetMiddleware([
                'cacheTime' => Configure::read('Asset.cacheTime')
            ]))

            // Add routing middleware.
            // Routes collection cache enabled by default, to disable route caching
            // pass null as cacheConfig, example: `new RoutingMiddleware($this)`
            // you might want to disable this cache in case your routing is extremely simple
            ->add(new RoutingMiddleware($this, '_cake_routes_'))

            // Add csrf middleware.
            ->add(new CsrfProtectionMiddleware([
                'httpOnly' => true
            ]));

        return $middlewareQueue;
    }
```

## 参考 : Cake3.8とCake3.6のroutes.php

CakePHP3.8のroutes.phpは、middleware()でCSRFが適用されない代わりに、デフォルトのルーティングでCSRFが適用されている

```php:config/routes.php
Router::scope('/', function (RouteBuilder $routes) {
    // Register scoped middleware for in scopes.
    $routes->registerMiddleware('csrf', new CsrfProtectionMiddleware([
        'httpOnly' => true
    ]));

    /**
     * Apply a middleware to the current route scope.
     * Requires middleware to be registered via `Application::routes()` with `registerMiddleware()`
     */
    $routes->applyMiddleware('csrf');

    /**
     *
     */
    Router::prefix('admin', function ($routes) {
        $routes->connect('/', ['controller' => 'Pages', 'action' => 'index']);
        $routes->fallbacks(DashedRoute::class);
    });

    /**
     * Here, we are connecting '/' (base path) to a controller called 'Pages',
     * its action called 'display', and we pass a param to select the view file
     * to use (in this case, src/Template/Pages/home.ctp)...
     */
    $routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);

    /**
     * ...and connect the rest of 'Pages' controller's URLs.
     */
    $routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);

    /**
     * Connect catchall routes for all controllers.
     *
     * Using the argument `DashedRoute`, the `fallbacks` method is a shortcut for
     *
     * ```
     * $routes->connect('/:controller', ['action' => 'index'], ['routeClass' => 'DashedRoute']);
     * $routes->connect('/:controller/:action/*', [], ['routeClass' => 'DashedRoute']);
     * ```
     *
     * Any route class can be used with this method, such as:
     * - DashedRoute
     * - InflectedRoute
     * - Route
     * - Or your own route class
     *
     * You can remove these routes once you've connected the
     * routes you want in your application.
     */
    $routes->fallbacks(DashedRoute::class);
});
```

で、CakePHP3.6のroutes.phpは、middleware()でCSRFが適用されているので、デフォルトのルーティングにはCSRFは適用されていない

```php:config/routes.php
Router::scope('/', function (RouteBuilder $routes) {
    /**
     * Here, we are connecting '/' (base path) to a controller called 'Pages',
     * its action called 'display', and we pass a param to select the view file
     * to use (in this case, src/Template/Pages/home.ctp)...
     */
    $routes->connect('/', ['controller' => 'Pages', 'action' => 'display', 'home']);

    /**
     * ...and connect the rest of 'Pages' controller's URLs.
     */
    $routes->connect('/pages/*', ['controller' => 'Pages', 'action' => 'display']);

    /**
     * Connect catchall routes for all controllers.
     *
     * Using the argument `DashedRoute`, the `fallbacks` method is a shortcut for
     *    `$routes->connect('/:controller', ['action' => 'index'], ['routeClass' => 'DashedRoute']);`
     *    `$routes->connect('/:controller/:action/*', [], ['routeClass' => 'DashedRoute']);`
     *
     * Any route class can be used with this method, such as:
     * - DashedRoute
     * - InflectedRoute
     * - Route
     * - Or your own route class
     *
     * You can remove these routes once you've connected the
     * routes you want in your application.
     */
    $routes->fallbacks(DashedRoute::class);
});
```