FastRoute をプレーンに近い状態で使用する際の CORS 対策メモ

経緯

PHP のルータパッケージ FastRoute をほぼ素に近い状態(ペラペラのオレオレフレームワークへ組み込み)で使用し、 JavaScript の AJAX通信 用パッケージ ky から POST メソッドでデータの書き込みの通信を行おうとしたしたところ、 CORS で捕まってしまったので対応策をメモしておきます。

状況としてはこの記事の通信を行おうとしたときの話になります。

対策

とりあえずサクッと実装したかったので「これでひとまず回避した」というような方法になります。

index.php

まずはネット上の FastRoute を最低限で使う場合のサンプルコードにオレオレフレームワーク用のクラスの読み込みやインスタンス化等の処理を付け足した全部のアクセス処理をいったん受け付けるディスパッチャ部分。

今回の記事で一番フォーカスしたいのは FastRoute のハンドラの定義部分です。HTTPメソッドごと (POSTOPTIONS) に異なるメソッド(クラスのメソッド)を割り当てて、別処理を走らせるようにしました(両方ともメソッドなのでややこしい)。

<?php

declare(strict_types=1);

$APP_BASE_PATH = __DIR__ . DIRECTORY_SEPARATOR . '..' . DIRECTORY_SEPARATOR;

// require 系諸々

use FastRoute;
use Monolog\Logger;
use Monolog\Handler\RotatingFileHandler;
use App\Domain\DomainModelSample\DomainModelSample;
use App\Repository\DomainModelSample\RepositoryDomainModelSample;

// 読み込んだクラスのインスタンス化

$handlers = function(FastRoute\RouteCollector $r) use ($APP_BASE_PATH) {
    $r->addRoute(['POST'], $APP_BASE_PATH . 'domainmodel/postsample/id', 'App\Repository\DomainModelSample\RepositoryDomainModelSample::write');
    $r->addRoute(['OPTIONS'], $APP_BASE_PATH . 'domainmodel/postsample/id', 'App\Repository\DomainModelSample\RepositoryDomainModelSample::options');
};

$dispatcher = FastRoute\simpleDispatcher($handlers);

$uri = $_SERVER['REQUEST_URI'];
$method = $_SERVER['REQUEST_METHOD'];

$args = [];
switch ( $_SERVER['REQUEST_METHOD'] ) {
    case 'GET':
        $args = &$_GET;
        break;
    case 'POST':
        $args = &$_POST;
        break;
    default:
        $args = [];
        break;
}

if (false !== $pos = strpos($uri, '?')) {
    $uri = substr($uri, 0, $pos);
}
$uri = rawurldecode($uri);

$routeInfo = $dispatcher->dispatch($method, $uri);

switch($routeInfo[0]) {
    case FastRoute\Dispatcher::NOT_FOUND:
        $View->error(404);
        break;
    case FastRoute\Dispatcher::METHOD_NOT_ALLOWED:
        $allowedMethods = $routeInfo[1];
        $View->error(405);
        break;
    case FastRoute\Dispatcher::FOUND:
        $handler = $routeInfo[1];
        $vars = $routeInfo[2];
        list($class, $method) = explode('::', $handler, 2);
        $callable;
        switch ($class) {
            case 'App\Repository\RepositorySample':
                $callable = $RepositorySample;
                break;
            // 略

            default:
                $callable = new $class;
                break;
        }

        $View->show(
            call_user_func_array(
                [
                    $callable,
                    $method
                ],
                [
                    $vars
                ]
            )
        );
        break;
}

Repository/DomainModelSample/RepositoryDomainModelSample.php

別処理の実装部分がこちら。

options メソッドは、HTTPメソッド OPTIONS の方は最低限の HTTP 200 OK のレスポンスを返すだけのダミー的な処理になっています。

本番処理である write メソッド は HTTPメソッド POST を受け付けて実際に処理を流す方です。

<?php

declare(strict_types=1);

namespace App\Repository\DomainModelSample;


class DomainModelSample extends Model
{
    // コンストラクタ等、略

    /**
     * @return array
     */
    public function write(): array
    {
        $jsonStr = file_get_contents('php://input');
        $jsonArray = $this->helper->jsonDecode($jsonStr);
        if(array_key_exists('user_id', $jsonArray)) {
            $userId = (int)$jsonArray['user_id'];
        }
        if($userId === $some_value) {
            // some proccessing...
        }

        return $someArray;
    }
    /**
     * @return array
     */
    public function options(): array
    {
        $data = [
            'statusCode' => 200,
            'data'       => true,
        ];

        return $data;
    }
}

これで CORS を回避できました。実際は HTTPレスポンスヘッダ をセットする MVC の View ぽい部分もあります(下記)が、これを書いた上での上述の回避策を入れて漸く回避、という感じになりました。

<?php

declare(strict_types=1);

namespace App;

use Lukasoppermann\Httpstatus\Httpstatus as HttpStatus;
use Monolog\Logger;

class View
{
    protected $helper;
    protected $origin;
    protected $Httpstatus;

    // コンストラクタ等は略

    /**
     * @return bool
     */
    public function setHeaders(): bool
    {
        header('Content-Type: application/json');
        header('Access-Control-Allow-Credentials: false');
        header('Access-Control-Allow-Origin: ' . $this->origin);
        header('Access-Control-Allow-Headers: X-Requested-With, Content-Type, Accept, Origin, Authorization');
        header('Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS');
        header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
        header('Cache-Control: post-check=0, pre-check=0');
        header('Pragma: no-cache');

        return true;
    }
}

とりあえずの実装なのでセキュリティは考えていない点はご留意を。

参考

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。