クソ雑魚エンジニアのメモ帳

学んだことを書くところ

開発環境のためのCORS有効化(phalconFrameWorkとwebpackとaxios)

背景

趣味で作っているwebアプリが、フロントエンドとバックエンドをRESTfulでつないでいる

  • フロントエンド側はvueとwebpackを使っていて、dev環境では、ビルドしなくてもローカルサーバー立ち上がってほっとりローディングですぐに修正を確認できる
  • サーバーサイドでは、dockerを使っているので、dockerコマンド一つでサーバーが立ち上がる。こちらも即座に反映される(当たり前)

これらの特徴から、フロントエンド内での開発PDCA・サーバーサイド内での開発PDCAはだいぶ早いと思っている。

ただ、フロントエンドとサーバーサイドでの疎通確認がボトルネックになると考えていて、チームでの開発では、フロントエンドの開発者とサーバーサイドの開発者は異なることが多い。なので、疎通確認をしたいのに、フロントエンド(またはサーバーサイド)がうまく起動できない(泣)みたいな状況になると思った。PDCA劇遅の予感がする。

現状

フロントエンドとサーバーサイド両方のローカルサーバーを立ち上げてaxiosを使って疎通しようとしても、疎通できない。

セキュリティの観点から、CORSという機能は制限されているためらしい

オリジン間リソース共有 (Cross-Origin Resource Sharing, CORS) は、追加の HTTP ヘッダーを使用して、ユーザーエージェントが現在のサイトとは別のオリジン (ドメイン) のサーバーから選択されたリソースにアクセスする権限を得られるようにする仕組みです。ユーザーエージェントは、現在の文書のオリジンとは異なるドメイン、プロトコル、ポート番号からリソースを要求するとき、オリジン間 HTTP 要求を発行します。

例えば http://domain-a.com から読み込まれた HTML ページが、 <img> src で http://domain-b.com/image.jpg に対して要求を行う場合です。今日のウェブ上では、多くのページが CSS スタイルシートや画像、スクリプトといったリソースを、コンテンツ配信ネットワーク (CDN) などの別のドメインから読み込んでいます。

セキュリティ上の理由から、ブラウザーは、スクリプトによって開始されるオリジン間 HTTP 要求を制限しています。例えば、 XMLHttpRequest や Fetch API は同一オリジンポリシー (same-origin policy)に従います。これは、これらの API を使用するウェブアプリケーションは、 CORS ヘッダーを使用しない限り、アプリケーションが読み込まれたのと同じドメインからしかリソースを HTTP で要求できないことを意味します。

https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control

なので、phalconフレームワーク側でCORSの制限を緩和する必要がある。あくまで開発環境の時のみ。

仕様の調査

axiosから、クロスドメインなURLを叩くと、勝手にpreflightが飛んでいくようである

f:id:Kouchannel55:20180516225556p:plain

公式にも書いてあった。今回はJSONでやりとりするため当然のようだ

https://developer.mozilla.org/ja/docs/Web/HTTP/HTTP_access_control#Examples_of_access_control_scenarios

webpackでのproxy設定

簡単な修正方法があった! :surfer:

https://www.yoheim.net/blog.php?q=20170803

webpackの設定ファイルに以下を追記。

    proxyTable: {
      '/api/*': {
        target: 'http://localhost:8111',
        secure: false
      }
    },

僕の場合はこうなった(ほぼテンプレですが)▼

index.js▼

'use strict'

// see http://vuejs-templates.github.io/webpack for documentation.
const path = require('path')

module.exports = {
  build: {
    env: require('./prod.env'),
    index: path.resolve(__dirname, '../../phalcon/public/index.html'),
    assetsRoot: path.resolve(__dirname, '../../phalcon/public'),
    assetsSubDirectory: 'static',
    // assetsPublicPath: '/',  TODO GithubPagesにあげるtめに修正
    assetsPublicPath: '',
    productionSourceMap: true,
    // Gzip off by default as many popular static hosts such as
    // Surge or Netlify already gzip all static assets for you.
    // Before setting to `true`, make sure to:
    // npm install --save-dev compression-webpack-plugin
    productionGzip: false,
    productionGzipExtensions: ['js', 'css'],
    // Run the build command with an extra argument to
    // View the bundle analyzer report after build finishes:
    // `npm run build --report`
    // Set to `true` or `false` to always turn it on or off
    bundleAnalyzerReport: process.env.npm_config_report
  },
  dev: {
    env: require('./dev.env'),
    port: 8080,
    autoOpenBrowser: true,
    assetsSubDirectory: 'static',
    assetsPublicPath: '/',
    proxyTable: {
      '/api/*': {
        target: 'http://localhost:8111',
        secure: false
      }
    },
    // CSS Sourcemaps off by default because relative paths are "buggy"
    // with this option, according to the CSS-Loader README
    // (https://github.com/webpack/css-loader#sourcemaps)
    // In our experience, they generally work as expected,
    // just be aware of this issue when enabling this option.
    cssSourceMap: false
  }
}

結局だめだった。そもそもリクエスト先が変更されていない。。。

f:id:Kouchannel55:20180516225455p:plain

f:id:Kouchannel55:20180516225527p:plain

愚直にサーバーサイド側でCORSを有効化することにした

phalconでのCORSを有効化(サーバーサイド)

公式で発見。

CorsMiddleware.php

<?php

use Phalcon\Events\Event;
use Phalcon\Mvc\Micro;
use Phalcon\Mvc\Micro\MiddlewareInterface;

/**
 * CORSMiddleware
 *
 * CORS checking
 */
class CORSMiddleware implements MiddlewareInterface
{
    /**
     * Before anything happens
     *
     * @param Event $event
     * @param Micro $application
     *
     * @returns bool
     */
    public function beforeHandleRoute(Event $event, Micro $application)
    {
        if ($application->request->getHeader('ORIGIN')) {
            $origin = $application->request->getHeader('ORIGIN');
        } else {
            $origin = '*';
        }

        $application
            ->response
            ->setHeader('Access-Control-Allow-Origin', $origin)
            ->setHeader(
                'Access-Control-Allow-Methods',
                'GET,PUT,POST,DELETE,OPTIONS'
            )
            ->setHeader(
                'Access-Control-Allow-Headers',
                'Origin, X-Requested-With, Content-Range, ' .
                'Content-Disposition, Content-Type, Authorization'
            )
            ->setHeader('Access-Control-Allow-Credentials', 'true');
    }

    /**
     * Calls the middleware
     *
     * @param Micro $application
     *
     * @returns bool
     */
    public function call(Micro $application)
    {
        return true;
    }
}

app.phpまたはmodule.php(ルーティング)▼

/**
 * Create a new Events Manager.
 */
$eventsManager = new Manager();
$application   = new Micro();

$eventsManager->attach('micro', new CorsMiddleware());
$application->before(new CorsMiddleware());

$application->setEventsManager($eventsManager);

https://docs.phalconphp.com/cs/3.1/application-micro#middleware-events-api-cors

しかしこれでは動かなかった :cry: 。原因はbeforeHandleRouteに処理が走っていないためみたいだが、根が深そうなので他の方法を探すことにした。

phalconForumから発見

https://forum.phalconphp.com/discussion/443/enable-cross-origin-resource-sharing

どうやら、プリフライト時と、その後どちらのリクエストでも、Access-Control-Allow-XXXXXをつける必要があるらしいので以下の様にした。

optionsの範囲とかヘッダーパラメータの付け方とかガバガバなのはスルーしてください :smirk:

$app->options('/(.*)', function () use ($app) {
    $app->response->sendHeaders();
});

app.php(diまわり)▼

$di->setShared('response', function () {
    $response = new Response();
    if (getenv("MODE") == "development") {
        $response->setHeader('Access-Control-Allow-Origin', '*')
            ->setHeader(
                'Access-Control-Allow-Methods',
                'GET,PUT,POST,DELETE,OPTIONS'
            )
            ->setHeader(
                'Access-Control-Allow-Headers',
                'Origin, X-Requested-With, Content-Range, ' .
                'Content-Disposition, Content-Type, Authorization'
            )
            ->setHeader('Access-Control-Allow-Credentials', 'true');
    }
    return $response;
});

f:id:Kouchannel55:20180516225621p:plain

OPTIONSとPOSTが正常系で帰ってる!

完成!!ここまで来るのに4時間はかかった。。。

所感

  • CORSのめんどくささを体感した
  • proxyServerがなぜ効かないのかは謎。誰か教えて :crying_cat_face:
  • ただ一つ不満を言うなら、以下の様にaxios側でURLをdev環境とprod環境で分ける必要があること
    • baseのaxiosクラスをつくれば一元管理できるので、なんとか納得できる

dev環境▼

    return axios.post('http://localhost:8111/session/login', json, {
        headers: { 'Content-Type': 'application/json' }
      })

prod環境▼

    return axios.post('/session/login', json, {
        headers: { 'Content-Type': 'application/json' }
      })