Ruumarkerをリリースしました

本日、Ruumarker(ルーマーカー)というサービスをリリースしました。

入居時チェック表作成サービス「Ruumarker」

リリースについて

GitHub

https://github.com/kasai441/ruumarker

サービス概要

引越しした時、不動産管理者に入居時の部屋の状況を報告するための書類を作成するためのサービスです。

キズの位置とその写真が載っている「入居時チェック表」を作成してPDFでダウンロードできます。

もし入居時に部屋にキズがあった場合に、自分の過失でないことをあらかじめ説明しておけば、不当な修繕費の請求を避けられるかもしれません。

サービス構成

サービス構成

開発時期

2021年7月: サービス内容検討開始

2021年10月: 要件決定

2022年1月: 開発開始

2022年7月: リリース

開発背景

本サービスはフィヨルド・ブートキャンプの最終課題として開発しました。サービスの仕様・要件は当校のメンターに提案されたものです。また、開発手順は当校のカリキュラムに沿って開発され、各ステージにおいて多くのアドバイスをいただきました。

開発者

@kasai441

正味2年のシステム会社開発経験を経て、フィヨルド・ブートキャンプにて1年半に渡ってWEBアプリ開発の学習をしてきました。今後WEBアプリ開発の求職活動予定。元オンラインゲーム背景イラストレーター。

サービス紹介

Ruumarkerとは?

Ruumarkerは、入居時チェック表を作るサービスです。

入居時チェック表とは?

✅入居時にキズがあったら?

入居時に自分のつけていないキズや汚れがあったら、管理人や不動産会社に報告する必要があります。

✅どうして報告するの?

施工時についたキズなら直してもらえるかもしれません。賃貸なら敷金から修繕費が差し引かれるのを防げます。

✅どうやって作る?

1. 間取り図を用意

まずは部屋の間取り図を用意します。スマホのカメラで撮影できればOKです。

間取り図

2. チェック表を作成

「入居時チェック表を作成する」からチェック表作成を始めます。

間取り図アップロード
間取り図アップロード

間取り図にキズ情報を加えていきます。

キズ情報の追加
キズ情報の追加

3. 大家さんに提出

管理人や不動産会社にキズがあった旨を伝えて、チェック表を提出します。

チェック表 見本

drive.google.com

開発過程

エレベーターピッチ

自作サービス作成の最初のステップで、サービスの指針となる要件を決定します。これは、プロダクトオーナーに一言でサービス内容を提案・説明して採用してもらうという想定で、世の中に潜在する何らかの「問題」を提示し、それを「解決」するためのツールとしてのサービスを定義します。

何回か自分の提案がボツになった後、自分のアイディアでサービスを作ることを諦めて、思いつかない人用に用意されたサービス案の中から以下のサービスを作ることを決めました。

[新居チェック報告] というサービスは、[引っ越しをした際、新居に既に傷があった場合にそれを写真で撮って傷のあった場所と共に不動産屋に報告するのが困難な問題] を解決したい、[賃貸物件に引っ越したばかりの人] 向けの、[新居に既にあった傷とその位置を伝える書類作成]サービスです。ユーザーは [ブラウザ上で傷の位置とその写真が載っている書類を作成する]ことができ、[文章で傷の位置を伝えるの]とは違って、[アップロードした間取り図に傷の位置矢印で指定することができる]事が特徴です。

このサービスを作ろうと思った理由は、「あれば便利」と思えたこと、「ちょうど自分の技術より少し高度なことをしそう」と思ったことです。無理なく少し高いハードルを越えることで、実現性のあるステップアップをめざしました。

ペーパープロトタイプ

このステップでは画面設計を行いました。この段階では、デザインを詰めたり、UIについて最適化する必要はなく、画面に登場する要素をレイアウトして全体図を掴むのが目的となります。

今回はFigmaというオンラインのツールをつかって、以下のようにベタ書きで画面遷移図を書きました。(本来、Figmaは共通パーツを作ったりページ遷移に沿った画面作成ができますが、自分はそれらの機能は学ばず、単なるクラウドお絵かきツールとしてしか使っていません。)

ペーパープロトタイプ

この段階で決めた要素はほとんど最後まで残りましたが、遷移の順番やレイアウトは、開発を進めながら、技術的制約やコストやユーザービリティの観点で少しずつ異なっていきました。

技術検証

サービスの全容が固まったところで、実装のための技術を検証します。

フィヨルド・ブートキャンプでは、自作サービス課題に入る一つ前の課題で、実際に運用しているアプリのスクラム開発に参加します。

GitHub - fjordllc/bootcamp: プログラマー向けEラーニングシステム

このアプリのサービス構成は、それまでの課程で学んできた言語とフレームワークを使ったものになっています。主に

を使用して実装を実践経験することができます。

新しいサービスを作ろうとする時は、スクラム開発課題での経験を引き継ぐ形で考えられます。そこから拡張して、どの程度独自性を発揮するかは、学習コストと技術アピールの兼ね合いで決まってくると思います。自分で調べて新たなスキルを導入すれば、より幅広いスキルをアピールできるでしょう。しかし、それ相応の学習コストがあるため、完成のための時間を余計にかけられるかどうかを見極める必要があります。

今回、自分のサービスの仕様を考えた時、経験していない実装方法、あるいは実装のための環境構築として以下のようなものがありました。

  1. 画像編集の機能をどのように実現するか
  2. RailsJavaScriptの連携をどのように実現するか
  3. フロントエンドのフレームワークを何にするか

これらについては、自分なりの技術選定を行う必要がありました。

1. 画像編集の機能をどのように実現するか

画像編集の機能は、ユーザーがアップロードした画像を拡大縮小したり回転したり上下左右をトリミングしたりできることが望まれました。

JSについてはDOM操作をほとんどしたことがなかったので、はじめはこれらを技術的に実現する方法が思いつきませんでした。メンターにインタビューするとCanvas APIというJavaScriptとHTML要素によるグラフィックを描画する方法があることを教えてもらいました。

おなじ課題に取り組んでいるブートキャンプの先輩のコードを覗いてみると、Canvasを簡単に操作できるJavaScriptのライブラリを使っているようでした。

JavaScriptのDOM操作については、Web書籍の『Eloquent JavaScript』やチュートリアル・サイトの『javascript.info』でも紹介されおり、Canvasの他にもSVGというマークアップ言語においても画像描画が可能なことを知りました。

これらのライブラリを使うかどうかに限らず、DOM要素を利用した何らかの方法で画像編集が実現できそうでした。具体的に何を使うかは確定していなかったのですが、検証で時間がかかりすぎていたので、「できそう」という手応えが得られた時点で技術的な検証はOKと判断しました。

最終的にはライブラリを利用しないDOM操作がメインで使用されましたが、Canvasは画像の回転や容量制限に用いられ、SVGはフォントのデザインに利用されました。いずれも何らかの形で採用されており、ここでの技術検証は非常に役立ちました。

2. RailsJavaScriptの連携をどのように実現するか

Railsは近年までWebpackerというライブラリを利用してJavaScriptのコードをコンパイルしてフロントエンドとの連携を構成するのが主流でした。ところが2021年の末に発表されたRailsのバージョン7において、Webpackerを公式にサポートせず、Webpacker自体の開発も終了されることが判明しました。

見本となるブートキャンプアプリは、Rails6 + Webpacker + Vue.jsの構成でしたが、すでにRails7のアナウンスが発表された後だったので、この構成はのちのち変更が必要なことがわかっていました。

そこで、今回は、学習コストをはらって、自分で調べてRails7を採用することに決めました。JavaScriptとの連携方法はこの時点では決断していません。

3. フロントエンドのフレームワークを何にするか

この段階ではフロントエンドのフレームワークにまで考えを巡らせることができませんでした。とにかく手を動かさないとモチベーションが保てなかったので、サーバーサイド開発を始めてしまいます。

サーバーサイド

かんばん

GitHubのプロジェクトツールを利用してまずは看板の作成をしました。これはGitHubのIssueやPull Requestと連動させてタスク管理できるツールで、開発初期の計画作りからリリース後のメンテナンスまで一貫して同じ手続きで管理していくことができます。

https://github.com/kasai441/ruumarker/projects/1

このツールが優秀なこと、加えるべきタスクの例がカリキュラムの課題文で提示されていたことから、非常に簡単にプロジェクトを起こすことができました。

やるべきことを箇条書きするだけで、思ったよりモチベーションが上がったので、とりあえず書き出してしまうのが良い方向につながりました。

サーバーサイド開発

プロジェクト立ち上げから、テストまでのサーバーサイドの行程は『現場で使える Ruby on Rails 5速習実践ガイド』を大いに参考にしました。この本については、フィヨルド・ブートキャンプ内のコミュニティで行われていた輪読会に参加して、精読したことがあったので、この本にそって開発を進めました。何も知らないところから実装まで、丁寧に解説されているため、難易度のレベル感があっていて、楽しく作業できました。

既に学んだことを思い出すのは楽しいです。今までカリキュラムで学んだことが、アプリ開発によってスムーズに実践されていきました。

  • DB設計、モデル作成
  • URL設計、ルーティング
  • コントローラー、仮のビュー作成
  • RspecによるE2Eテスト
  • AWS S3への接続
  • Herokuへのデプロイ

フロントエンド

フロントエンドの技術検証

先述の通り、フロントエンドの技術検証がしんどかったので、サーバーサイド開発を先にやってしまって、後回しにしていました。いざフロントの開発という段になって検証を始めたら、けっこう大変な作業となってしまいました。

まずは以下のような、サーバーサイドとフロントエンドの組み合わせを選択肢として挙げ、一つずつ吟味することにしました。

  1. Rails7 + バニラJS
  2. Rails7 + Webpack + Vue.js
  3. Rails6 + Webpacker + Vue.js
  4. Rails7 + Stimulus

実際に動かしてみないとストレスが溜まっていくので、それぞれのパターンのプロジェクトをデモ用のレポジトリーに構築して、起動できるかを確認しました。それぞれのファイル構成・内容の差分を確認していき、どうやら「2. Rails7 + Webpack + Vue.js」の組み合わせで構築できそうだと分かりました。

採用理由はやや混雑した思考で、

  • 新しいRails7が動かしたい
  • 昔からあるWebpackを知りたい
  • Vueができるからやっとく

みたいな感じでした。一度触ったことがあるものをもう少し詰めてみたくなる気持ちと、新しいものを知りたいという気持ちが混在しています。

詳細は以下の記事にまとめています。

Railsにどのようにフロントエンドを連携させるかの技術検証っぽいこと(Rails7 + Webpack + Vue.js3)

画像アップロードの実装

すでにサーバーサイド開発の時点で画像アップロード機能はRailsのみで実現できていました。しかし、Vue.jsでフォーム画面を作ると、入力された情報をJavaScriptAjax通信によってRailsへリクエストする必要が出てきました。幸い、次のブログ記事に、ほぼ同じ技術構成で画像をアップロードする機能の実装方法が書かれていたので、ほぼ丸ごと踏襲して実装しました。

【Rails API + Vue】Active Storageを使って画像をアップロード・表示する

Ajax通信のリクエストにはaxiosというライブラリを使っています。これで、FormDataクラスで作成したデータをそのままPUTリクエストに仕込むことができました。Railsへの書き込みリクエストで必要となるCSRFトークンをヘッダーに仕込む仕組みもaxiosを利用したものが多く検索でヒットしたので苦労なく実装できました。

この記事で使われているVuexは採用しませんでした。Vuexの公式のアナウンスで、複雑でないアプリでは非推奨と書いてあったことと、なるべく基本的なVueのSFCのデータ交換の仕組みを学びたかったためです。

if your app is simple, you will most likely be fine without Vuex https://vuex.vuejs.org/#when-should-i-use-it

画像をドラッグ&ドロップで移動して保存する。

フロントエンドで、ひいてはこのアプリ全体の技術の中で、最も重要で未知数だった実装は、画像のドラッグ&ドロップ編集機能でした。

この技術が使用される画面は二つあります。

  • 画像トリミング

間取り図とキズの写真をトリミングするために、ドラッグ&ドロップで画像を動かせるようにしたいです。目指したのはTwitterの画像編集画面です。

画像トリミング

  • キズ位置

間取り図の中にキズの位置を指定するために、ベースの画像をドラッグ&ドロップしてポイントを指定するようにしたいです。目指したのはタクシー配送アプリのGOです。

キズ位置

画像トリミングだけなら、それ専用のJavaScriptのライブラリがあったのですが、キズ位置の実装を実現するライブラリはうまく見つけられませんでした。そこで、キズ位置の実装は自力でコードを書くことにしましたが、そうすると画像トリミングの機能も似ているので、共通で同じ処理を書けばよく、ライブラリは使わない方向に決めました。

そのかわり、ここはシンプルなJavaScriptのコードを書きました。

この記事にはHTMLエレメントを直接スタイルによって動かす方法が紹介されています。これをVue.js上で実現できるように応用しました。

Vue.jsでのイベントハンドラの書き方はこちらを参考にしました。

JavaScriptのテスト

JavaScriptのE2Eテストには、Rspec・Capybaraを使いました。Capybaraは高機能で、ドラッグ&ドロップの動作もテストで実現できました。詳細は以下のIssueに記録しました。

https://github.com/kasai441/ruumarker/issues/112

Canvasでのリサイズ

大きな容量の画像を指定のサイズまで縮小するために、Canvasを利用しました。画像ファイル情報を読み込んで、所定のサイズで再描画する処理となりました。以下の記事を参考にしてCanvasの操作を実装しました。

大きな画像をJavaScriptでリサイズしてからAjax送信する方法 - CONSOLE DOT LOG

ここでは、生成された画像をVue.jsでの同期的な処理で行っていましたが、同様の処理を切り出して使い回すために、非同期処理に直す必要があったので、以下の記事を参考にして、メソッド切り出しを行いました。

PromiseベースでImage().onloadする

Capybaraではスタイルを当てられたあとの画像サイズは簡単に取得できるのですが、この機能のテストでは画像の元サイズが変更されているかどうかを確かめる必要があります。元サイズを参照するには、ブラウザ操作ツールのSeleniumにおいてJavaScriptを操作して、画像のnaturalWidthを取得することで実現できました。

it '画像幅が元のままとなる' do
  show_image = page.find_by_id('show-image')
    expect(execute_script('return arguments[0].naturalWidth', show_image)).to eq 88
end

このSeleniumJavaScriptを実行させる方法は次の記事で見つけました。苦労して検索したので、貴重な情報なのではないかと思います。

How To Test For Broken Images

実装時の記録は以下のIssueになります。

https://github.com/kasai441/ruumarker/issues/131

デザイン

CSSフレームワークにはTailwindを採用しました。

技術選定と環境構築

Vue.jsのバージョン3に対応していて、cssbundlingで簡単に実装できそうなTailwindを選択しました。

以下が、CSSフレームワークの技術検証とTailwindインストールのIssueです。 https://github.com/kasai441/ruumarker/issues/84

cssbundling-railsの公式ガイドに沿ってビルドはかなりスムーズにできました。jsbundling-railsを先にビルドしていたので、環境が揃っていたことが大きかったです。

Tailwindの使用感

TailwindはCSSファイルを全く自分では触らないで、すべて用意されたクラスをHTMLタグに書き込んで、内部的にCSSを生成させるフレームワークです。クラスがTailwind特有なので学習コストがかかるっぽく見えますが、クラスの命名CSSのプロパティから推測されやすくなっているので、ドキュメントは参照する程度で、使用感はフルスクラッチCSSを書いている感覚になります。クラス命名のクセを覚えたあとは、読むのも容易いので、作業中にフレームワークのクセで詰まるシーンはほとんどなかったです。

セキュリティ

セキュリティについては既にブログの記事にしているので以下に示します。それぞれの概要は次のとおりです。

CORS

CORS(オリジン間リソース共有)の設定について調べています。Vue.jsとRailsでの通信でクロスオリジンが発生する構成にしていないので、ほとんど場合、クロスオリジンは発生しません。そこでCORSの設定はデフォルトで、基本はクロスオリジンを許可しない設定にしました。

開発後半で、AWS S3において、画像を回転するとCORSに引っかかる、という不具合が発生しました。これは、AWS側の設定において回避できましたが、CORSについて事前に学んでおいたのでスムーズにバグ解消できたのがよかったです。

該当Issue: https://github.com/kasai441/ruumarker/issues/241

CSP

CSPはWebブラウザで使える機能を制限する機能ですが、デフォルトでは全てOFFになっているので、アプリが動く程度まで強い制限に設定し直しました。

ランダムURL

このアプリではユーザーのログインなしでサービスを利用できるようにしてユーザビリティを高めています。ユーザー情報のプライバシーはURLをランダムにすることで事実上他人にはアクセスできないようにして実現しました。このため、実際にはユーザーデータは公開されています。その脆弱性について検討したのが以上の記事です。

この記事を読んでもらってメンターから検索避けをしたほうがよいというアドバイスをいただきました。検索避けの実装は以下のIssueになります。

https://github.com/kasai441/ruumarker/issues/178

まとめ

長期間にわたって開発をおこなったのでこの記事をまとめることで自分でも忘れていたところを思い出すきっかけになりました。

この記録によって今後のWEBサービス開発作成をしている方の助力になれば幸いです。かなり端的に書いたので分かりにく所があればご質問お寄せください。

また、このサービスはなるべくなら今後も不足している部分をアップデートしていけたらと思っています。OSSなので何か改善案があればご意見お寄せください。

Railsにどのようにフロントエンドを連携させるかの技術検証っぽいこと(Rails7 + Webpack + Vue.js3)

この記事は、自作サービスRuumarkerのリリースに先立って、2022年2-3月にメモしたものを記事にしたもの。一つのリリース記事では収めると長すぎるので、フロントエンドの技術検証っぽい部分だけ切り出した。

場当たり的に行ったことの羅列にはなってしまうけど、これからWebサービスRails7 + Webpack + Vueで作る人や、jsbundling-railsを使おうとしている人あたりが参考になるかもしれないと思い、とりあえず文字起こししてみる。

最終的な構成

最終的に今回開発したWEBサービスは技術構成は以下のようになった。

自作サービス構成図

  • Railsのバージョン → 7
  • Vue.jsのバージョン → 3

情報収集:フロントエンドの選択肢

Rails7でサーバーサイドを作ってからフロントエンドのことを考え始めた

自作サービス開発のプロセスは、

仕様決定 → 画面設計 → 技術検証 → サーバーサイド開発 → フロントエンド開発

が基本路線ではあるが、事前のフロントエンドの技術検証がしんどくて手が止まりそうだったので、とりあえずフロントエンドのことは考えずに先にサーバーサイドの開発をしてしまった。

そして、いざフロントエンドを実装、という段になって、はじめて本気で技術調査に乗り出すことになった。

Stimulusって? Hotwire?

フロントエンドの開発に入ってから改めてGemを眺めると、Stimulusとかいう見慣れないライブラリがあらかじめ入っていた。

## Hotwire's modest JavaScript framework [https://stimulus.hotwired.dev]
gem "stimulus-rails"

Hotwireって何?

これらはRails7のデフォルトのnewを実行した結果、インストールされていたものらしい。別に使わないでも他に方法があるらしい。

軽く調べてみただけでもフロントエンドの選択肢がわりと沢山ありそう。わけがわからなくなってきたので、次のように整理してみた。

あり得そうな選択肢

  1. Rails7 + バニラJS
  2. Rails7 + Webpack + Vue.js (jsbundling-rails)
  3. Rails6 + Webpacker + Vue.js
  4. Rails7 + Hotwire + Stimulus

1. Rails7 + バニラJS

フレームワークを使わずに書くJavaScriptのことをバニラJSと呼ぶらしい。

『流麗なJavaScript』という誤植だらけだけど原本がロングセラーな本がある。これの輪読会に参加していて、JavaScriptを基本的なところから学び直しており、素のJSを使いたい思いが強かったので、まずこれができないか考えた。

しかし、この場合、フレームワークを特に使う必要がない分、ビルドをどうしていいのか分からなかった。Rails+Webpacker+Vue.jsなら似たようなサービスが多いので、見様見真似で構築できそうだけど、Rails+バニラJSという構成だと見習うモデルが見当たらない。

2. Rails7 + Webpack + Vue.js (jsbundling-rails)

こちらは伝統的なWebpackを使い続けたい人のための、Rails7が用意した解答の一つみたいなやつ。

Vue.jsは少し書いたことがあるので採用したくもあるけど、じゃあどうやってビルドするの?と考えると、ドキュメントが充実しているもののひとつとしてWebpackがある。

Nodeレスに移行するまでのつなぎとしての "bundling-rails" gem

jsbundling-railsというライブラリによって、Webpackで生成したアセットをRailsに取り込むことができるようになっているとのこと。

詳しくは分からないけど、Rails7でサポートされているのが心強い。これができれば、Node.jsのほうでも経験値が溜まってよさそう。

3. Rails6 + Webpacker + Vue.js

こちらは最も保守的な構成。

この構成のアプリは主流なので手本となるコードやドキュメントが豊富。安定した環境構築が期待できる。

しかし、核となるRubyライブラリのWebpackerがRails7からサポートされなくなり、Webpacker自体の開発も終了することが明らかになっているので、リリース後のどこかのタイミングで、サービス構成を大幅に見直す必要が出てくるのが難点。

さよならWebpacker

ちなみにWebpackはNode.jsのライブラリ。WebpackerはWebpackをRubyでラップしたRails用のライブラリで別物。

この場合は、Rails6へのダウングレードをする必要があるのでRails7の将来的学習コストを負債として積み残すことになる。

4. Rails7 + Hotwire + Stimulus

Rails7でデフォルトでインストールされるフレームワーク。ビルドがほぼすでに自動で終わっているので、環境構築の学習コストが低い。

とはいえ、Stimulusがそれ特有の書き方をするので、学習コストはある。しかも、まだあまり広まってないので、果たしてそのスキルが今後活きるのかどうかが疑問。

Rails 7.0正式リリース、Node.js不要のフロントエンド開発環境がデフォルトに

デモ:それぞれ実際に動かす

やはり触って動かさないとストレスが溜まるので、とりあえず空のプロジェクトを作って動かしてみる。

Rails new でいろいろ試す

Rails 7 をちょこっと試す(さらば、Webpacker 編) - Qiita

この記事に沿って実際にデモアプリを作って、Rails newして、ビルドでの差分をgit diffを使って確認してみることにした。細かい作業の手順は以下のIssueに時系列で記録している。

https://github.com/kasai441/ruumarker/issues/82

rails newは、"."を指定すると現在のディレクトリに作成してくれるので、app、bin、configあたりを消して再実行すると何度も同じディレクトリでnewすることができ、それをcommitに記録して差分を確認できるようにしている。各コミットは以下の通り。

いろいろなRails newのコミット

https://github.com/kasai441/demo7/commits/main

オプションなしで 'rails new . --database postgresql'

これは、Rails7のデフォルト設定。先述の「4. Rails7 + Hotwire + Stimulus」の構成であり、Stimulusが作成される。

'rails new . --database postgresql -j esbuild'

これは、先述の「2. Rails7 + Webpack + Vue.js (jsbundling-rails) 」の、Webpackの代わりにesbuildを使うパターン。esbuildのほうが速くオススメらしいので試してみた。

esbuildというnpmがpackage.jsonに追記されている模様。 このままだとbin/devが動かないが、手動でpackage.jsonにscriptsを追加したところ、bin/devが動くようになった。

{
  "scripts": { "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds" },
  ...
}

しかしながら、esbuildではVue.jsのプラグインができないようだったので、Vue.jsを入れるならば次のWebpackがやはり本命となってくる。

'rails new . --database postgresql -j webpack --skip-sprockets'

これは、先述の「2. Rails7 + Webpack + Vue.js (jsbundling-rails) 」の構成。 --skip-sprockets をしてもsprocketsをインストールする。webpackで圧縮したアセットはRailsのSprocketを使ってコンパイル(?)されるのでSprocketは必須だが、この技術検証の時点ではよく分かっていなかった。

情報収集・デモ:Vue3を動かす

Webpackの採用が濃厚に

ここまできて、Rails7 + Webpack + Vue3 という選択肢が濃厚に。採用理由はやや混雑した思考で、

  • 新しいRails7が動かしたい
  • 昔からあるWebpackを知りたい
  • Vueができるからやっとく

みたいな感じだった。一度触ったことがあるものをもう少し詰めてみたくなる気持ちと、新しいものを知りたいという気持ちが混在している。

Vueがうまくビルドできたら、もうそれを採用ということで、Vueの構築をしてみる。

Rails7 + Webpack + Vue3の調査と構築

まずは、webpack + vue に絞ってドキュメントを漁る。記事のリンクは以下のIssueに記録されている。

webpack + Vue で動作確認 · Issue #83 · kasai441/ruumarker

ここで改めて知ったこととして、Webpackはあまり新しいものではないらしく、2、3年前の記事がたくさん出ていることがわかる。esbuildが速くてよいがvue.jsのプラグインが使えないからダメっぽいことも知る。

jsbundling-railsのビルドはwikiに詳しかった。

jsbundling-rails/switch_from_webpacker.md at main · rails/jsbundling-rails

日本語はこちら

次の記事は、RailsとVue.jsの構成でビルドしている模範となった。

スクラッチからWebpack+Babel+Vue 3を使う - Qiita

Vue3の公式インストールガイドも確認のために参照した。

Vueが動いた時、はじめて安心した。けっこう時間をかけて調査をしたので動かなかったらキツイところだった。この瞬間に、もうこのサービス構成でいくことで決定していたと思う。

実装:途中まで作っていた自作サービスに導入

https://github.com/kasai441/ruumarker/issues/87

こちらのIssueでは、Rails7でRspecやCIまで導入済みの状態から、jsbundling-railsを利用してWebpackとBabelとVueを導入する流れが記録されている。

Webpackはすぐ動くようになった。ところが、Rspecで全落ちしてしまう。importmapがうまく動いていないようなので、迷った末、削除することに。importmapはwebpackと役割が一緒なのか、rails new -j webpackでは生成されていないことから、捨ててよいと判断した。理由はよくわからないが、これでテストが落ちなくなった。

StimulusやTurboを動かすためにはyarnから@hotwired/stimulusや@hotwired/turbo-railsをインストールする必要があるっぽい。Sitimulusはいらないような気がしたが、この時点では残した。

Herokuへのデプロイは全くつまずかなかった。デプロイできて、これまた一安心。開発環境→テスト→本番と、つまずきポイントがいっぱいあったので、苦しかった。

技術検証っぽいことができたような

これまでは、誰かのサービス構成を猿真似しているばかりだった。今回もだいぶ真似はしたが、少し選択的なことを考えたので、らしくなってきたかもしれない。

サービス構成図を書くツール「Diagrams」をいじってみた

こういう感じのサービス構成図が簡単に書きたい。 

自作サービス構成図

何か簡単に書けるツールはないかと探したら、以下の記事が。

Python コードでアーキテクチャ図を生成できる Diagrams がめっちゃ便利!

Python使ったことないけどいじりたくなったので、とりあえずPythonのインストールから

Python3をHomebrewでインストール

python使ったことないけど、たしかHomebrewでインストールされてた記憶なので確認

python -V

しかし、そんなコマンドないです、と言われて出てこない。あれ??

Homebrewでインストールされているパッケージ一覧を見てみる。

brew list

brew list
おるおる。で、調べてみたら、

python -V

ではなく

python3 -V

でインストールされていることが確認できた。3系は別パッケージという扱いなのかな??

今回の目的はpipにて使いたいgraphvizというパッケージがあるので、pipについても同様に確認。

どうやら、pipも pip3 -Vみたいな感じで3系が別名パッケージになっている。

必要パッケージのインストール

さてpython3とpip3があるのを確認できたので次の準備として、Graphvizというパッケージをインストールする。

参考: https://diagrams.mingrammer.com/docs/getting-started/installation

brew install graphviz

以上で準備完了なのでdiagramsをインストール。pipの人はpipで。pip3の人はpip3で。

pip3 install diagrams

チュートリアル

さっきのdiagrams公式にチュートリアルがのっているので動作確認がてらやってみる。

pythonファイルを作成

touch diagram.py

チュートリアルにあるコードを書く。

# diagram.py
from diagrams import Diagram
from diagrams.aws.compute import EC2
from diagrams.aws.database import RDS
from diagrams.aws.network import ELB

with Diagram("Web Service", show=False):
    ELB("lb") >> EC2("web") >> RDS("userdb")

これで実行してみると、

 python3 diagram.py 

以下のようにpngファイルが作られている。

$ ls
diagram.py  web_service.png

web_service.png

できた!

簡単な操作方法

上記コードにだいたいの基本が詰まっている。

from diagrams import Diagram

にて基本のメソッドを呼び出す。

with Diagram("Web Service", show=False):

「Web Service」の部分は任意。これでインデントを下げて要素をくっつけていく。

たとえばEC2なら、

from diagrams.aws.compute import EC2

でライブラリからインポートして、

 EC2("web") 

で呼び出せる。出力すると以下のような図になる。 

png

要素同士を繋ぐには以下のようにする。

ELB("lb") >> EC2("web")

png

また、グループ化したい場合は

with Cluster("グループ名"):

でClusterをつくり、インデントを下げて要素をくっ付ければ良い。

importのしかた

from [PATH] imoport [NAME] のかたちで、用意されたアイコンをインポートできる。PATHはDiagramsのメニューから、以下のカテゴリーを選ぶと一覧が出てくる。 

Diagramsのメニュー

例えば、「Programming」から以下のようなフレームワークを選べる。 

programming.framework

結構使えるものが限られるので、ないやつはCustomで用意するしかない。

Custom要素の作り方

参考: https://diagrams.mingrammer.com/docs/nodes/custom

画像をプロジェクト内に配置して、第2引数にパスを渡せば良い。

例えばcc_heart.black.pngというファイルをmy_resourcesというディレクトリに配置し、以下のように呼び出す。

# インポート
from diagrams.custom import Custom

# 呼び出し
  cc_heart = Custom("Creative Commons", "./my_resources/cc_heart.black.png")

自分のサービスのサービス構成図を作ってみた

以下のような感じで冒頭の自作サービスのサービス構成図を作ってみた。

from diagrams import Cluster, Diagram
from diagrams.custom import Custom
from diagrams.programming.language import JavaScript
from diagrams.programming.framework import Vue
from diagrams.programming.language import Nodejs
from diagrams.programming.framework import Rails
from diagrams.programming.language import Ruby
from diagrams.onprem.database import PostgreSQL
from diagrams.aws.storage import S3
from diagrams.onprem.ci import GithubActions
from diagrams.onprem.vcs import Git

with Diagram("Ruumarker", show=False):
  js = JavaScript("JavaScript")

  with Cluster("デザイン"):
    daisyui = Custom("DaisyUI", "./my_resources/daisyui.png")
    tailwind = Custom("Tailwind", "./my_resources/tailwind.png")
    
  with Cluster("フロントエンド"):    
    vue = Vue("Vue.js")
    webpack = Custom("Webpack", "./my_resources/webpack.png")
    babel = Custom("Babel", "./my_resources/babel.png")
  
  with Cluster("サーバーサイド"):
    nodejs = Nodejs("Node.js") 
    rails = Rails("Rails")
    ruby = Ruby("Ruby")
    rubocop = Custom("Rubocop", "./my_resources/rubocop.png")
  
  rspec = Custom("Rspec", "./my_resources/rspec.png")
  
  eslint = Custom("ESLint", "./my_resources/eslint.png")

  database = PostgreSQL("PostgreSQL")

  store = S3("AWS S3")

  heroku = Custom("Heroku", "./my_resources/heroku.png")

  with Cluster("GitHub"):
    ci = GithubActions("GitHubActions")
    git = Git("Git")

  eslint >> js >> vue >> webpack >> nodejs >> rails >> heroku
  daisyui >> tailwind >> webpack
  babel >> webpack
  rubocop >> ruby >> rails
  rspec >> rails
  rspec >> vue
  rails >> ci >> git
  rails << database
  rails << store

アイコンの使用許可

CustomでのアイコンはそれぞれのサービスのBranding Guidelinesなどを参考にした。大体は「フェアユース」なら許されそう。ここらへん、あまり時間を使ってないのできっちりしなければならない場合は、読み込みが必要。

Tailwind https://tailwindcss.com/brand

Webpackなど、OpenJSファンデーションのパッケージ https://openjsf.org/wp-content/uploads/sites/84/2021/01/OpenJS-Foundation-Trademark-Policy-2021-01-12.docx.pdf

Bable https://github.com/babel/babel/discussions/11731

Rubocop https://docs.rubocop.org/rubocop/about/logo.html


ESLint
 https://eslint.org/branding/


Heroku 
https://brand.heroku.com

RspecとDaisyUIはガイドライン不明

【Rails】CSPの設定をした

CSPのJSの実行の制限について調査して設定する。

デフォルトではCSPはコメントアウトされている。JSに関するポリシーを有効化する。そうするとJSを使っているテストが落ちるようになるので、report_onlyをtrueにして詳しいエラーメッセージを確認する。

Rails.application.configure do
  config.content_security_policy do |policy|
    # policy.default_src :self, :https
    # policy.font_src    :self, :https, :data
    # policy.img_src     :self, :https, :data
    # policy.object_src  :none
    policy.script_src  :self, :https # ここだけ有効化
    # policy.style_src   :self, :https
    # Specify URI for violation reports
    # policy.report_uri "/csp-violation-report-endpoint"
  end

  config.content_security_policy_report_only = true
end

サーバーを立ち上げてページにアクセスすると以下のようなエラーが。

[Report Only] Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src 'self' https:".

‘unsafe-eval’で調べると、これを指定しない場合に以下のようなJSを弾く設定になるとのこと。

eval()
Function()
setTimeout()
setInterval()
...

参考: CSP: script-src - HTTP | MDN

実際のコンソールではこれだけのエラーが出ている。

https://s3.us-west-2.amazonaws.com/secure.notion-static.com/cc600d0c-eef9-4b59-9fa6-38b5af532649/Untitled.png?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Content-Sha256=UNSIGNED-PAYLOAD&X-Amz-Credential=AKIAT73L2G45EIPT3X45%2F20220412%2Fus-west-2%2Fs3%2Faws4_request&X-Amz-Date=20220412T052925Z&X-Amz-Expires=86400&X-Amz-Signature=ca6c8b5c54cec66ca2883458a9616f31f03c33862f0e7ac132aa4b9f7ccd43b1&X-Amz-SignedHeaders=host&response-content-disposition=filename%20%3D%22Untitled.png%22&x-id=GetObject

リンクを辿ると、application-なんたら.jsの中で、evalメソッドが大量に使われているので、これが引っかかっているようだ。これは、webpackで生成されたスクリプトで絶対に出てきそうなコードなので、設定する必要がありそう。

なので、‘unsafe-eval’を指定。

policy.script_src  :self, :unsafe_eval, :https

参考: Rails アプリケーションのセキュリティ対策(CORS/CSP/HSTS)

これでエラーが出なくなり、テストも通るようになった。

ちなみにHTTPのレスポンスヘッダーが以下のように指定されている。

Content-Security-Policy: script-src 'self' 'unsafe-eval' https:

ランダムURLのセキュリティ

現在のサービスの要件が、ログインの煩わしさを省いてユーザビリティを高める目的で、ログイン機能なし、ランダムURLによってアクセスを絞り込むのみ、というものになっている。

これについて、現行の実装と、考えられるセキュリティリスクをリストアップし、これに対する機能追加の方法についても吟味する。そのコストや効果を検討し、実装方針を固めることにした。

現行の仕様

現行では、以下のようなランダムなIDを自動生成してDBに文字列として保存している。

self.id = SecureRandom.alphanumeric(20)

それをそのままルーティングにすることで以下のようなURLを実現している。

http://localhost:3000/rooms/UiC8Z6pYgQnz8RMhqWg8

基本的にはこのURLさえ知ることができれば、作成者以外でも作成者の情報を閲覧することができる。

このサイトのデータベースに登録される秘匿性の高い情報は以下の通り。

  • Eメール(他の情報との組み合わせ次第で個人情報となる)
  • 部屋の間取り図(十分なデータを持っている場合、住所を特定できるかも??)
  • 部屋のキズの写真(個人情報が写真に紛れる可能性?)

それほど致命的な個人情報はないが、漏洩すれば怒られそうなものもある。

現行では、漏洩のリスクを明記して、他者へURLを渡さないよう注意喚起をすることで、これらの漏洩リスクを許容してもらうサービスとなっている。また、データベースの保存期間を短くする(10日)ことで、長期間リスクにさらされる状況を避けている。

考えられるセキュリティリスク

リスク1: ユーザーがURLを公開してしまう。

リスク2: リクエスト情報からURLが露見してしまう。

例えば以下の場所を閲覧されてしまえばURLが参照できるそうだ。

Webサーバのアクセスログ
ファイアウォールログ
プロキシサーバのキャッシュやログ
ブラウザのキャッシュや履歴

参考: 1-4. クエリストリングから情報が漏れる

リスク3: DBから露見してしまう。

現行ではURLで使用されているランダム文字列がそのままIDとして保存されているため。

参考: https://railstutorial.jp/chapters/advanced_login?version=5.0#sec-remember_token

リスク4: 通信を傍受される。

HTTPSによるSSLでの対策はしている。

参考: https://railstutorial.jp/chapters/advanced_login?version=5.0#sec-remember_token

対応策としての機能追加

上記のリスクに対応するためには以下のような機能追加が考えられる

対応策1: DBのハッシュ化

DBに不正アクセスされた場合にURLを盗まれてしまうと自由にアクセスできるようになってしまうので、これをハッシュ化する。

参考: 第9章 発展的なログイン機構 - Railsチュートリアル

参考: ハッシュ化・ハッシュ値ってなんだ?から、パスワードの仕組みまで。 – 自主的20%るぅる

IDをハッシュ化すると再利用不能になるので、IDと別にURLというカラムを作り、それをハッシュで保存する。

ルーティングでURLにID以外を使うことはできそう。

参考: Railsのルーティングでid以外の数値カラムをURLに使うときの注意点 - Qiita

ID+URLのようなPARAMを渡して、コントローラーでモデルのauthenticated?を呼び出す。

# controller
room = Room.find_by(id: params[:id])
room.authenticated?(params[:url])

モデルでは、ハッシュ化されて保存されているurl_digestとリクエストで渡されたurlをハッシュ化したものを比較して同一かどうか調べるようにする。

# model
def authenticated?(url_token)
    BCrypt::Password.new(url_digest).is_password?(url_token)
end

この場合、現行のままDBに侵入された時のリスクのみ回避できる。

対応策2: URLとクッキーのトークンの二つが揃わないと所定のページが閲覧できないようにする。

同じブラウザのみでの閲覧を許し、URLが知られても他人にはアクセスできなくする。複数ブラウザをまたいで閲覧できなくなり、ブラウザ間で同期しているブックマークが機能しなくなるという悪い副作用がある。

個別ページのURLを作成したタイミングで、クッキーにトークンを保存し、トークンをハッシュ化したダイジェストをDBに保存する。毎回アクセス時にURLの保持するダイジェストとブラウザのクッキーが保持するトークンが一致していることを確認するようにする。

参照: 第9章 発展的なログイン機構 - Railsチュートリアル

対応策3: メールで保存したURLにトークンを仕込んでメールからは所定のページが閲覧できるようにする

本来はログイン機能の登録時にメールでの認証を行う機能を流用し、トークン付きのリクエストを仕込んだリンクをメールによってユーザーの手元に保管させる。URLを知っていてもトークンがない場合はアクセスできなくする。複数ブラウザをまたぐことも可能になる。

メールを発行したタイミングでDBにトークンをハッシュ化したダイジェストを保存する。トークンを仕込んだリンクがメール本文に記載されている。アクセス時にはURLの保持するダイジェストとトークンが一致していることを確認するようにする。

参照: 第11章 アカウントの有効化 - Railsチュートリアル

対応策4: ログイン機能を追加

いっそのことログイン機能を実装してしまう。ユーザーのないサービスなので、各ページにログインする機構となる。

この場合、一般的なログインの実装が使えるので、設計コストがないが、対応策の中では一番大掛かりな機能追加となる。また、後述するように、本来の仕様や設計思想には反する実装となる。

セキュリティ効果とユーザビリティとコストの検証

セキュリティを高めようとすればするほど、ログインの機能をそなえたサービスに近くなり、本来の要件である、ログインの煩わしさを省いてユーザビリティを高める目的と矛盾するようになる。ランダムURLの採用は、ログインの代替を期待していたので、その本来の役割も失われる。

ただし、現行でもEメールの登録はほとんど必須なので、パスワードのないログイン機能の追加は、現行と比べてそこまでユーザビリティを下げないかもしれない。むしろ、機能追加のコストをかけるべきかという観点の方が重要になってくる。

もしコストを許容するならば、ランダムURLは閲覧のみ、編集・管理権限はログインによって与えられる、という役割分担はありえる。あるいは、Eメール登録すれば文書保存でき、Eメールなしならその場で破棄するという場合分けによって、ユーザーの選択肢を増やすという機能追加はあり得る。

結論

リスクを許容してもらうように注意喚起をする現行の状態が一番コストが低い。追加のセキュリティ対策をリリース後に回していくのが、自作サービス課題の要件とも一致するように感じた。

【Railsセキュリティ検証】no-corsリクエスト、DNSリバインディング、本番環境のSSL

no-cors

先日の続き。

下記のようにnor-corsモードでリクエストすると、クロスオリジンでもブロックされなくなるとのことなので、セキュリティの穴にならないか調べた。

リクエストをno-corsモードで送ると、opaqueレスポンスが返ってくる。中にはデータは入っていないとのことなので、気にしなくてよさそう。

fetch('http://localhost:3001/api/rooms/TIciF1vZjzUfq4dqpHKQ/maps/106.json', {
    method: "GET", 
    mode: "no-cors",
})
.then(data => console.log(data))

参考: Note that mode: "no-cors" only allows a limited set of headers in the request:

参考: fetchのmodeについて - Qiita

ほとんどの場合、opaqueレスポンスは使えないとのこと。

参考: What is an opaque response? by Tamas Piros

DNSバインディング

IPアドレスは偽装できるので同一オリジンのリクエストに見せかけることができるという話。

⇒ ダミーVirtualHostによって防いでいるが、よくわからない

⇒ 確認のcurl

curl -v -H 'Host: evil.example.com' http://$(dig weak.example.jp | grep -v '^;' | grep 'A' | awk '{print $5}')

⇒ ブラウザベース、DNSベースでの防御策があるとのこと。サーバーベースでは、HTTPSを推奨している。これは、やっているので、できることは十分やっているという理解。

本番環境でのSSL

通信中にデータを盗む攻撃から守るために本番環境でSSLを設定する。Herokuデプロイの場合は、サーバー側の設定もよしなにやってくれるらしい。

https://railstutorial.jp/chapters/sign_up?version=5.0#sec-professional_grade_deployment

config/environments/production.rb

**Rails.application.configure do.
  .
  .
  *# Force all access to the app over SSL, use Strict-Transport-Security,
  # and use secure cookies.*  
  config.force_ssl = true
  .
  .
  .
end**

【Rails】ブラウザのコンソールからのfetchによって、CORS設定の挙動を確認する

Rails: 7.0.2.3

Ruby: 3.1.0

Railsのgem ‘rack-cors’ の動作がよくわからないので、動作確認してみた。HTTPの知識が浅いので動作確認の方法がなかなかわからなかったので、きちんと手順どおりに記録することにした。

確認したいのは、’rack-cors’の gem がなくてもクロスオリジンのリクエストを受け付けないこと。現在作成しているアプリでは Vue.js からリクエストすることはあるが、同一オリジン内でのやり取りに限定されるので、わざわざクロスオリジンを許可する必要がないから。

前提:

  • RailsプロジェクトでAPIのルーティングとコントローラーを用意している。
# config/routes.rb
Rails.application.routes.draw do
  namespace :api do
    resources :rooms, only: '' do
      resources :maps, only: %i[show create update delete]
    end
  end
end
# app/controllers/api/maps_controller.rb

module Api
  class MapsController < ApplicationController
    def show
      @map = Map.find(params[:id])
      render :show
    end

    private

    def map_params
      params.require(:map).permit(:trimming, :expansion, :rotation, :image)
    end
  end
end
# app/views/api/maps/show.json.jbuilder

json.merge! @map.attributes
json.image_url url_for(@map.image)
# app/views/maps/edit.html.slim

div
  h1 マップ画像を編集します

  #maps-edit(room_id="#{@map.room.id}")

  = link_to 'ルームに戻る', room_path(@map.room)
// app/javascript/maps/edit.js

import { createApp } from 'vue'
import MapsEdit from './edit.vue'

document.addEventListener('turbo:load', () => {
  const selector = '#maps-edit'
  const maps_edit = document.querySelector(selector)

  if (maps_edit) {
    const roomId = maps_edit.getAttribute('room_id')
    const app = createApp(MapsEdit)
    app.provide('roomId', roomId)
    app.mount(selector)
  }
})
// app/javascript/maps/edit.vue

<template>
  <section id="maps-edit">
    <div v-if="isImageEdit" @touchmove.prevent>
      <image-edit></image-edit>
      <image-update></image-update>
    </div>
    <div v-else>
      <image-show></image-show>
    </div>
  </section>
</template>

...省略...
  • Vue.js からRailsAPIへリクエストがあるが、上記の仕組みから、同一オリジンとなる。
// app/javascript/components/image_show.vue

<template>
...省略...
</template>

<script>
import api from '../modules/api'

export default {
  name: 'ImageShow',
  ...省略...
  async created() {
    const response = await api.actions.show(`/api/rooms/${this.roomId}/${this.targetModel}s/${this.mapId}.json`)
  }
}
</script>
// app/javascript/modules/api.js

import axios from 'axios'

const actions = {
  async show(url) {
    try {
      const response = await axios.get(url)
      return response.data
    } catch (e) {
      console.error(e)
    }
  },
    ...省略...
}

export default {
  namespaced: true,
  actions
}

参考: https://github.com/kasai441/ruumarker

‘rack-cors’がある場合とない場合、’rack-cors’での設定を変えた場合の3パターンで検証した。以下がそれぞれの期待の動作。

  1. rack-cors gem を入れないデフォルトの状態では、同一オリジン以外からのリクエストがブロックされる。
  2. rack-cors にて、リクエストを許可する origins を限定した時、指定以外のオリジンからのリクエストがブロックされる。また、レスポンスのヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin がそれぞれ設定通り表示される。
  3. rack-cors にて、 origins を全て許可する’*’で指定した時、クロスオリジンでリクエストができる。また、レスポンスのヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin がそれぞれ設定通り表示される。

これらが確認できれば、rack-cors gemを採用しなくともセキュアである根拠となる。

fetchの方法

HTTPの知識が浅いので、自在にリクエストを飛ばすことができずに苦労した。とりあえずヘッダーの必要のないGETで動作確認をしたくて色々調べていて以下のやり方に辿り着いた。

いろいろなオリジンからリクエストをする方法でわかりやすかったのは、異なるWEBアプリ(RailsでもVue CLIでもNode.jsでも)を別ポートで立ち上げ、ブラウザのdev-toolからJavaScriptでfetchによってリクエストするというもの。そうするとNetworkタブでResponseのヘッダーも確認できる。

  • Railsのプロジェクトを複数立ち上げ
# ProjectA
rails server -p 3000
# ProjectB
rails server -p 3001
  • ブラウザのdev-toolでfetch

例えば http://localhost:3000 から http://localhost:3001APIへリクエストしたい場合は、ブラウザで http://localhost:3000 にアクセスして、dev-toolのコンソールを開き、以下のようなJavaScriptを実行する

fetch('http://localhost:3001/routes', {
    method: "GET", 
})
.then(res => res.json())
.then(data => console.log(data))

参考: RailsでAPIにCORSを設定する - Qiita

これを上記のパターンごとに実行し、動作結果をそれぞれ確認した。

1. rack-cors gem を入れない状態では、同一オリジン以外からのリクエストができない

  • Gemfile から’rack-cors’を外し、bundle install する。
  • config/initializers/cors.rb がある場合、削除する。
  • rails server を起動する。(port: 3001)

1-a. クロスオリジンのリクエスト(ポート3000からポート3001)

localhost:3000 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて下記のJavaScriptを実行する。 この場合、api/rooms/{:room_id}/maps/{map_id}.jsonというAPIを叩いている。

1

リクエストがCORSでブロックされたので期待通り。

2

また、CORSの設定がないので当然、レスポンスヘッダーには Access-Control-Allow-Methods と Access-Control-Allow-Origin がない。

1-b. 同一オリジンからのリクエスト(ポート3001 からポート3001)

ブラウザでlocalhost:3001にアクセスして、コンソールから先ほどと全く同じ JavaScript を実行する。

これは、リクエストが通り、レスポンスが帰ってくる。CORSの設定がないのでレスポンスヘッダーも期待通り。

2. rack-cors にて origin を限定した時、指定以外のオリジンからのリクエストができない

  • Gemfile に’rack-cors’を追加し、bundle install する。
  • config/initializers/cors.rb を以下のように設定する。
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins 'localhost:3002' # ポート3002からのリクエストのみ許可
    resource '*',
         headers: :any,
         methods: %i[get post put delete]
  end
end
  • rails server を立ち上げる。(port: 3001)

2-a. 指定外オリジンからのリクエスト(ポート3000 からポート3001)

localhost:3000 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて下記のJavaScriptを実行する。

クロスオリジンでCORSでも設定されていないポート3000からのリクエストはブロックされる。期待通り。

2-b. 同一オリジンのリクエスト(ポート3001 からポート3001)

localhost:3001 にブラウザでアクセスしたのち、dev-toolのコンソールを開いて同様のJavaScriptを実行する。

レスポンスヘッダーには Access-Control-Allow-Methods と Access-Control-Allow-Origin がないが、同一オリジンなので、CORSに設定されていないがリクエストは通る。期待通り。

2-c. 指定オリジンからのリクエスト(ポート3002 からポート3001)

localhost:3002 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて同様のJavaScriptを実行する。

クロスオリジンだが、CORSで指定したオリジンからのリクエストなので、レスポンスヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin が記載されており、リクエストが通る。これが’rack-cors’の期待通りの動作。

3. rack-cors にて origin を’*’で指定した時、クロスオリジンでリクエストができる

  • Gemfile に’rack-cors’を追加し、bundle install する。
  • config/initializers/cors.rb を以下のように設定する。
    Rails.application.config.middleware.insert_before 0, Rack::Cors do
      allow do
        origins '*' # 全てのオリジンを許可
        resource '*',
                 headers: :any,
                 methods: %i[get post put delete]
      end
    end
  • rails server を立ち上げる。(port: 3001)

3-a. クロスオリジンのリクエスト(ポート3000 からポート3001)

localhost:3000 に別アプリのサーバーを立ち上げて、ブラウザでアクセスしたのち、dev-toolのコンソールを開いて下記のJavaScriptを実行する。

CORSに全てのオリジンを指定しているのでレスポンスヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin が記載されており、リクエストも通る。

3-b. 同一オリジンのリクエスト(ポート3001 からポート3001)

localhost:3001 にブラウザでアクセスしたのち、dev-toolのコンソールを開いて同様のJavaScriptを実行する。

同一オリジンの場合はレスポンスヘッダーは変更されないようだ。いずれにしても、この場合リクエストは通る。

結果

以上で考えられる全てのパターンで動作確認できた。どうやら同一オリジン間でのリクエストには’rack-cors’は働かないようで、レスポンスヘッダーが変化しなかった。クロスオリジンのリクエストの場合、cors.rb で指定したオリジンのみ、レスポンスヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin が記載され、リクエストが許可されることが確認できた。

Railsではデフォルトでクロスオリジンリクエストがブロックされているようで、わざわざ’rack-cors’を入れなくてもセキュアだし、’rack-cors’を導入して全てのオリジンを許可する設定にしたりすると、かえってセキュリティが甘くなることも分かってよかった。

CORSはリクエストを完全にブロックできない

当初は、CORSが外部からのリクエストを完全にコントロールしてくれるものと思っていた。しかし、同一オリジンであればブロックされないので、例えば今回のようにWEBアプリのアドレスに直接アクセスした状態でJavaScriptを実行すればリクエストは可能。

または、curlで以下のようにリクエストしてもGETできるようだ。

curl -v http://127.0.0.1:3001/api/rooms/TIciF1vZjzUfq4dqpHKQ/maps/106.json
*   Trying 127.0.0.1:3001...
* Connected to 127.0.0.1 (127.0.0.1) port 3001 (#0)
> GET /api/rooms/TIciF1vZjzUfq4dqpHKQ/maps/106.json HTTP/1.1
> Host: 127.0.0.1:3001
> User-Agent: curl/7.79.1
> Accept: */*
> 
* Mark bundle as not supporting multiuse
< HTTP/1.1 200 OK
< X-Frame-Options: SAMEORIGIN
< X-XSS-Protection: 0
< X-Content-Type-Options: nosniff
< X-Download-Options: noopen
< X-Permitted-Cross-Domain-Policies: none
< Referrer-Policy: strict-origin-when-cross-origin
< Content-Type: application/json; charset=utf-8

...中略...

< Vary: Origin
< Transfer-Encoding: chunked
< 
* Connection #0 to host 127.0.0.1 left intact
{"id":106,"trimming":"{\"x\":116,\"y\":-62}", ...中略...}

なので、APIでやり取りするデータは公開されているものとして扱うべきだと思った。すくなくともCORSはそれを隠蔽するような機能ではなく、あくまで他オリジンからのリクエストをブロックする機能にのみ働くので、たとえばCSRFを防ぎたいという意図なら、CSRF-TOKENをサーバーサイド側で認証するなどの機能をONにしておく必要がある。これまたRailsではデフォルトでそうなっている。

参考にしたサイト

Rails Dockerでリクエストとばして確認

Rails以外の、リクエストとレスポンスの挙動の話 設定の仕方は出ていない

Ajax通信で、と書いてある

⇒ 昔はJavaScriptから他のAPIへリクエストする手段がなかったがCORSがそれを解決した、とある