Ruumarkerをリリースしました
本日、Ruumarker(ルーマーカー)というサービスをリリースしました。
リリースについて
GitHub
https://github.com/kasai441/ruumarker
サービス概要
引越しした時、不動産管理者に入居時の部屋の状況を報告するための書類を作成するためのサービスです。
キズの位置とその写真が載っている「入居時チェック表」を作成してPDFでダウンロードできます。
もし入居時に部屋にキズがあった場合に、自分の過失でないことをあらかじめ説明しておけば、不当な修繕費の請求を避けられるかもしれません。
サービス構成
開発時期
2021年7月: サービス内容検討開始
2021年10月: 要件決定
2022年1月: 開発開始
2022年7月: リリース
開発背景
本サービスはフィヨルド・ブートキャンプの最終課題として開発しました。サービスの仕様・要件は当校のメンターに提案されたものです。また、開発手順は当校のカリキュラムに沿って開発され、各ステージにおいて多くのアドバイスをいただきました。
開発者
正味2年のシステム会社開発経験を経て、フィヨルド・ブートキャンプにて1年半に渡ってWEBアプリ開発の学習をしてきました。今後WEBアプリ開発の求職活動予定。元オンラインゲーム背景イラストレーター。
サービス紹介
Ruumarkerとは?
Ruumarkerは、入居時チェック表を作るサービスです。
入居時チェック表とは?
✅入居時にキズがあったら?
入居時に自分のつけていないキズや汚れがあったら、管理人や不動産会社に報告する必要があります。
✅どうして報告するの?
施工時についたキズなら直してもらえるかもしれません。賃貸なら敷金から修繕費が差し引かれるのを防げます。
✅どうやって作る?
1. 間取り図を用意
まずは部屋の間取り図を用意します。スマホのカメラで撮影できればOKです。
2. チェック表を作成
「入居時チェック表を作成する」からチェック表作成を始めます。
間取り図にキズ情報を加えていきます。
3. 大家さんに提出
管理人や不動産会社にキズがあった旨を伝えて、チェック表を提出します。
開発過程
エレベーターピッチ
自作サービス作成の最初のステップで、サービスの指針となる要件を決定します。これは、プロダクトオーナーに一言でサービス内容を提案・説明して採用してもらうという想定で、世の中に潜在する何らかの「問題」を提示し、それを「解決」するためのツールとしてのサービスを定義します。
何回か自分の提案がボツになった後、自分のアイディアでサービスを作ることを諦めて、思いつかない人用に用意されたサービス案の中から以下のサービスを作ることを決めました。
[新居チェック報告] というサービスは、[引っ越しをした際、新居に既に傷があった場合にそれを写真で撮って傷のあった場所と共に不動産屋に報告するのが困難な問題] を解決したい、[賃貸物件に引っ越したばかりの人] 向けの、[新居に既にあった傷とその位置を伝える書類作成]サービスです。ユーザーは [ブラウザ上で傷の位置とその写真が載っている書類を作成する]ことができ、[文章で傷の位置を伝えるの]とは違って、[アップロードした間取り図に傷の位置矢印で指定することができる]事が特徴です。
このサービスを作ろうと思った理由は、「あれば便利」と思えたこと、「ちょうど自分の技術より少し高度なことをしそう」と思ったことです。無理なく少し高いハードルを越えることで、実現性のあるステップアップをめざしました。
ペーパープロトタイプ
このステップでは画面設計を行いました。この段階では、デザインを詰めたり、UIについて最適化する必要はなく、画面に登場する要素をレイアウトして全体図を掴むのが目的となります。
今回はFigmaというオンラインのツールをつかって、以下のようにベタ書きで画面遷移図を書きました。(本来、Figmaは共通パーツを作ったりページ遷移に沿った画面作成ができますが、自分はそれらの機能は学ばず、単なるクラウドお絵かきツールとしてしか使っていません。)
この段階で決めた要素はほとんど最後まで残りましたが、遷移の順番やレイアウトは、開発を進めながら、技術的制約やコストやユーザービリティの観点で少しずつ異なっていきました。
技術検証
サービスの全容が固まったところで、実装のための技術を検証します。
フィヨルド・ブートキャンプでは、自作サービス課題に入る一つ前の課題で、実際に運用しているアプリのスクラム開発に参加します。
GitHub - fjordllc/bootcamp: プログラマー向けEラーニングシステム
このアプリのサービス構成は、それまでの課程で学んできた言語とフレームワークを使ったものになっています。主に
- Ruby on Rails
- JavaScript
- Vue.js
を使用して実装を実践経験することができます。
新しいサービスを作ろうとする時は、スクラム開発課題での経験を引き継ぐ形で考えられます。そこから拡張して、どの程度独自性を発揮するかは、学習コストと技術アピールの兼ね合いで決まってくると思います。自分で調べて新たなスキルを導入すれば、より幅広いスキルをアピールできるでしょう。しかし、それ相応の学習コストがあるため、完成のための時間を余計にかけられるかどうかを見極める必要があります。
今回、自分のサービスの仕様を考えた時、経験していない実装方法、あるいは実装のための環境構築として以下のようなものがありました。
- 画像編集の機能をどのように実現するか
- RailsとJavaScriptの連携をどのように実現するか
- フロントエンドのフレームワークを何にするか
これらについては、自分なりの技術選定を行う必要がありました。
1. 画像編集の機能をどのように実現するか
画像編集の機能は、ユーザーがアップロードした画像を拡大縮小したり回転したり上下左右をトリミングしたりできることが望まれました。
JSについてはDOM操作をほとんどしたことがなかったので、はじめはこれらを技術的に実現する方法が思いつきませんでした。メンターにインタビューするとCanvas APIというJavaScriptとHTML要素によるグラフィックを描画する方法があることを教えてもらいました。
おなじ課題に取り組んでいるブートキャンプの先輩のコードを覗いてみると、Canvasを簡単に操作できるJavaScriptのライブラリを使っているようでした。
JavaScriptのDOM操作については、Web書籍の『Eloquent JavaScript』やチュートリアル・サイトの『javascript.info』でも紹介されおり、Canvasの他にもSVGというマークアップ言語においても画像描画が可能なことを知りました。
これらのライブラリを使うかどうかに限らず、DOM要素を利用した何らかの方法で画像編集が実現できそうでした。具体的に何を使うかは確定していなかったのですが、検証で時間がかかりすぎていたので、「できそう」という手応えが得られた時点で技術的な検証はOKと判断しました。
最終的にはライブラリを利用しないDOM操作がメインで使用されましたが、Canvasは画像の回転や容量制限に用いられ、SVGはフォントのデザインに利用されました。いずれも何らかの形で採用されており、ここでの技術検証は非常に役立ちました。
2. RailsとJavaScriptの連携をどのように実現するか
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速習実践ガイド』を大いに参考にしました。この本については、フィヨルド・ブートキャンプ内のコミュニティで行われていた輪読会に参加して、精読したことがあったので、この本にそって開発を進めました。何も知らないところから実装まで、丁寧に解説されているため、難易度のレベル感があっていて、楽しく作業できました。
既に学んだことを思い出すのは楽しいです。今までカリキュラムで学んだことが、アプリ開発によってスムーズに実践されていきました。
フロントエンド
フロントエンドの技術検証
先述の通り、フロントエンドの技術検証がしんどかったので、サーバーサイド開発を先にやってしまって、後回しにしていました。いざフロントの開発という段になって検証を始めたら、けっこう大変な作業となってしまいました。
まずは以下のような、サーバーサイドとフロントエンドの組み合わせを選択肢として挙げ、一つずつ吟味することにしました。
- Rails7 + バニラJS
- Rails7 + Webpack + Vue.js
- Rails6 + Webpacker + Vue.js
- Rails7 + Stimulus
実際に動かしてみないとストレスが溜まっていくので、それぞれのパターンのプロジェクトをデモ用のレポジトリーに構築して、起動できるかを確認しました。それぞれのファイル構成・内容の差分を確認していき、どうやら「2. Rails7 + Webpack + Vue.js」の組み合わせで構築できそうだと分かりました。
採用理由はやや混雑した思考で、
- 新しいRails7が動かしたい
- 昔からあるWebpackを知りたい
- Vueができるからやっとく
みたいな感じでした。一度触ったことがあるものをもう少し詰めてみたくなる気持ちと、新しいものを知りたいという気持ちが混在しています。
詳細は以下の記事にまとめています。
Railsにどのようにフロントエンドを連携させるかの技術検証っぽいこと(Rails7 + Webpack + Vue.js3)
画像アップロードの実装
すでにサーバーサイド開発の時点で画像アップロード機能はRailsのみで実現できていました。しかし、Vue.jsでフォーム画面を作ると、入力された情報をJavaScriptでAjax通信によって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での同期的な処理で行っていましたが、同様の処理を切り出して使い回すために、非同期処理に直す必要があったので、以下の記事を参考にして、メソッド切り出しを行いました。
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
このSeleniumにJavaScriptを実行させる方法は次の記事で見つけました。苦労して検索したので、貴重な情報なのではないかと思います。
実装時の記録は以下のIssueになります。
https://github.com/kasai441/ruumarker/issues/131
デザイン
技術選定と環境構築
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を実行した結果、インストールされていたものらしい。別に使わないでも他に方法があるらしい。
軽く調べてみただけでもフロントエンドの選択肢がわりと沢山ありそう。わけがわからなくなってきたので、次のように整理してみた。
あり得そうな選択肢
- Rails7 + バニラJS
- Rails7 + Webpack + Vue.js (jsbundling-rails)
- Rails6 + Webpacker + Vue.js
- 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自体の開発も終了することが明らかになっているので、リリース後のどこかのタイミングで、サービス構成を大幅に見直す必要が出てくるのが難点。
ちなみに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に記録して差分を確認できるようにしている。各コミットは以下の通り。
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
 おるおる。で、調べてみたら、
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

できた!
簡単な操作方法
上記コードにだいたいの基本が詰まっている。
from diagrams import Diagram
にて基本のメソッドを呼び出す。
with Diagram("Web Service", show=False):
「Web Service」の部分は任意。これでインデントを下げて要素をくっつけていく。
たとえばEC2なら、
from diagrams.aws.compute import EC2
でライブラリからインポートして、
EC2("web")
で呼び出せる。出力すると以下のような図になる。 
要素同士を繋ぐには以下のようにする。
ELB("lb") >> EC2("web")

また、グループ化したい場合は
with Cluster("グループ名"):
でClusterをつくり、インデントを下げて要素をくっ付ければ良い。
importのしかた
from [PATH] imoport [NAME] のかたちで、用意されたアイコンをインポートできる。PATHはDiagramsのメニューから、以下のカテゴリーを選ぶと一覧が出てくる。 
例えば、「Programming」から以下のようなフレームワークを選べる。 
結構使えるものが限られるので、ないやつは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
【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
実際のコンソールではこれだけのエラーが出ている。
リンクを辿ると、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サーバのアクセスログ ファイアウォールログ プロキシサーバのキャッシュやログ ブラウザのキャッシュや履歴
リスク3: DBから露見してしまう。
現行ではURLで使用されているランダム文字列がそのままIDとして保存されているため。
参考: https://railstutorial.jp/chapters/advanced_login?version=5.0#sec-remember_token
リスク4: 通信を傍受される。
参考: 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:
ほとんどの場合、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 からリクエストすることはあるが、同一オリジン内でのやり取りに限定されるので、わざわざクロスオリジンを許可する必要がないから。
前提:
# 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)
- Vue.jsによる画面がRails内のJavaScriptから生成される。
# 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> ...省略...
// 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パターンで検証した。以下がそれぞれの期待の動作。
- rack-cors gem を入れないデフォルトの状態では、同一オリジン以外からのリクエストがブロックされる。
- rack-cors にて、リクエストを許可する origins を限定した時、指定以外のオリジンからのリクエストがブロックされる。また、レスポンスのヘッダーに Access-Control-Allow-Methods と Access-Control-Allow-Origin がそれぞれ設定通り表示される。
- 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:3001 のAPIへリクエストしたい場合は、ブラウザで 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を叩いている。
リクエストがCORSでブロックされたので期待通り。
また、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ではデフォルトでそうなっている。
参考にしたサイト
- curl コマンド 使い方メモ - Qiita
- Cross-Origin Resource Policy (CORP) - HTTP | MDN)
- 【Rails】APIにCORSの設定をする方法 | みんたく
⇒ Rails以外の、リクエストとレスポンスの挙動の話 設定の仕方は出ていない
⇒ Ajax通信で、と書いてある
⇒ 昔はJavaScriptから他のAPIへリクエストする手段がなかったがCORSがそれを解決した、とある