セッションCookieのHTTP Only属性とSecure属性の違い

先日セッションにまつわる言葉についてまとめましたが、ふと表題の件も理解が曖昧だなと気になったので調べてみました。

HTTP Only属性とは

cookie のスコープ(参照・操作の権限)を HTTP リクエストに制限するもの
CookieのSecure属性/HttpOnly属性の指摘と修正方法と脆弱性の解説

この「HTTPリクエスト」という言葉が引っかかって「Secure属性と何が違うの?🤔」となっていたのですが、ここでいうHTTPリクエストスクリプトによるリクエスト(XMLHttpRequest)に対する言葉。http or httpsは関係ありません。

つまり、HTTP Only属性はスクリプトからCookieへのアクセスを禁止してくれる属性。
もっと具体的にはXSS脆弱性があった際もCookieの盗み出しを予防してくれるもの(ただし、リスクの低減には役立つものの、完全に防衛できるわけではないそう)。

Secure属性とは

指定されたCookiehttpsの通信の時のみCookieを送信する
CookieのSecure属性/HttpOnly属性の指摘と修正方法と脆弱性の解説

プロトコルhttpの時はCookieを送信しないよ、という属性。

なお、この説明文中によく見かける「通信経路上にいる第三者」について、先輩が例を教えてくれました。

  1. ファーストフードやカフェのようなフリーWifiの提供されている場所で、似た名前(SSID)の罠Wifiを提供
  2. 利用者がそれと気づかず罠Wifiを利用
  3. Wifiの提供者は通信内容を盗聴し放題!👿

この時http通信だと盗まれてそのまま情報が見られるので、暗号化通信であるhttpsを利用しましょう、http通信の場合はCookieを送信しないSecure属性を利用しましょう、ということだそうです😊

実装例は?

Railsでの実装方法

  • HTTP Only属性
    デフォルトで設定されています。
    もし外したいような事象が出てきた場合は下記で行えますが、かなりレアなケースではないでしょうか。
# config/initializers/session_store.rb
# ダメ、ゼッタイ
MyApp::Application.config.session_store :active_record_store, key: "_myapp_session", httponly: false
  • Secure属性
    同じくsession_store.rbでsecure属性を設定できますが、ローカルでhttpで開発している場合、developmentの時は付与しないように設定します。
# config/initializers/session_store.rb
MyApp::Application.config.session_store :active_record_store, key: "_myapp_session", secure: (Rails.env.production? || Rails.env.staging?)

参考サイト

はじめてのFlutter〜環境構築後編〜

Flutterの環境構築のお話、後編です。
前編はこちらからどうぞ。

Steps

  1. Flutter SDKのインストール
  2. Android Studioのインストール <-いまここ
  3. XCodeのインストール

DLに時間がかかるものリスト

※ おそらく以前のバージョンだとDart単独でインストールする必要があったのだと思いますが、今はFlutterのSDKに含まれています。

Android Studioセットアップ(昨日からの続き)

下記の二記事を参考に進めます。
* Flutter開発環境構築(Mac編) - Qiita
* FirebaseとFlutterでアプリ開発してみる【其ノ二:Flutter&Dart篇】 - Qiita

  • 「Install Type」
    -> セットアップ方法をスタンダード/カスタムで選べる。ひとまずスタンダード。

  • 「Select UI Theme」
    -> UIテーマを選べる。暗い方が好みなのでDarculaを選択。

  • 「Verify Settings」
    今選択したセットアップ方法を確認して「Finish」。

セットアップ方法の指定が完了すると走るComponentのDLはやや時間がかかります。
また最後、HAXMのインストールをする際、パスワードが求められます。
パスワード入力してOKしたのだけど、処理が止まってしまって「Cancel」を押すしかない状態に。。。
ただ、DLは完了していたみたいでAndroid Studioを再起動したら普通に開けました。 HAXMがインストールされているか確認したいときは下記のコマンドで確認できます。

$ find ~/ -name sdk
# => /Users/makicamel//Library/Android/sdk

$ cd /Users/makicamel//Library/Android/sdk
$ ./emulator/emulator -accel-check
# => accel:
# => 0
# => HAXM version 7.3.2 (4) is installed and usable.
# => accel

Android Studioでプロジェクト作成〜仮想端末(Virtual Device)ダウンロード

Android Studioを再起動して(たぶん本来はFinished押せばいいだけ)「Start a new Android Studio project」をクリック。

OS(Phone and TabletとかWear OSとか)、テンプレートを選べます。
便利ですが、エミュレータが使いたいだけなのでどれでもOK。
名前、言語を適当に選択・記入して「Finish」するとAndroid Studio プロジェクトが作成されます。

IDEの画面が開きますが、Gradleのダウンロードが走るのでしばし待ちます。
この間暇なのでGradleとは?を調べる。OSSのビルドシステムなんですね。

完了したらエミュレータを動かします。
「Tools」->「AVD Manager」->「+ Create Virtual Device」をクリック、デバイス一覧が表示されるので適当なデバイスを選択します。今回はNexus 5Xを選択。

SystemImageを選択してと言われるので、適当に選択(API Level28)。Androidのキャラクタかわいいですね。
「Download」をクリックするとLicense Agreementに同意を求められるので同意します。またダウンロードが発生😣(900MB)
ダウンロードが完了したら自動でリフレッシュしてくれるので、選択して「Next」。
Configurationを確認して問題なければ「Finish」。やっと仮想端末ができました…!
今ダウンロードしたばかりの仮想端末が表示されるので、右側「Actions」からプレイボタン(▶)をクリックするとエミュレータが起動します!👏

VS CodeにFlutterプラグインをインストール

Gradleや仮想端末のダウンロードを待っている間、エディタの準備をしておきます。
VS Codeを起動してExtensions(Shift+Cmd+X)をクリックして「Flutter」を入力、一番上に出てきたものをインストール。

VS Codeを再起動してコマンドパレット(Shift+Cmd+P)で「doctor」を入力、flutter doctorしてVS Codeでエラーが出ていなければVS Codeの準備は完了です。

Android StudioでHello, world!してみる

仮想端末の準備ができたら、早速Hello, world!してみます!
ターミナルで適当なディレクトリに移動してコマンドを入力します。

$ cd ~/dev/src
$ flutter create hello_flutter
# => In order to run your application, type:
# =>   $ cd hello_flutter
# =>   $ flutter run

$ ls
# =>   hello_flutter

アプリを実行するにはflutter runしてね、と言われるので、実行します。

$ flutter run
# => No connected devices.

さっきダウンロードした端末と接続できてない様子。flutterに聞いてみます。

$ flutter doctor
[!] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
    ! Some Android licenses not accepted.  To resolve this, run: flutter doctor --android-licenses

ライセンスに同意する必要があるそうなので、同意します。

$ flutter doctor --android-licenses

ライセンスを確認しますか?と聞かれるのでyで答えてそれぞれ同意します。
再度flutterを実行!

$ flutter run
# => No connected devices.

むむむ。

$ flutter emulators
# => 1 available emulator:
# => Nexus_5X_API_28 • Nexus 5X • Google • Nexus 5X API 28
# => To run an emulator, run 'flutter emulators --launch <emulator id>'.
# => To create a new emulator, run 'flutter emulators --create [--name xyz]'.

ならば、とflutter emulators --launch Nexus_5X_API_28を入力すると、今立ち上げてるから二重起動はできないよ、と怒られます。うーん?

エミュレータを終了させて、再度flutter emulators --launch Nexus_5X_API_28を入力するとエミュレータが起動します。が、やはりdeviceが接続されていないよ、とのこと。
参考記事のようにもうひとつAndroid Emulatorを作成し、再起動後にflutter runすると、(This is taking an unexpectedly long time.)のメッセージ。
うーん…。

あれこれ試してみて、androidエミュレータを起動しているのにdevice not foundが出る問題を参考に、2つめに作った仮想端末をGUI(Android Studio)から起動してflutter runすると実行できることがわかりました。
謎事象だけど、実行を先にしたいので後で調べることにします。

今回はHello, Flutter!と画面に表示するのが目的なので、lib/main.dartファイルを編集します。
ホットリロードはターミナルでrを押してね、とのことなので、rを入力してファイルを保存。

children: <Widget>[
  Text(
    'Hello, Flutter for this many times:',
  ),
  Text(
    '$_counter',
    style: Theme.of(context).textTheme.display1,
  ),
],

無事ホットリロードされることを確認しました!👏👏👏

iOS環境を構築

お次はiOS環境の構築。
XCodeはAppStoreからインストール。

XCodeはインストール済だったので、早速Simulatorを立ち上げます。 XCodeのインストール方法はこちらに詳しいです。

なお、GUIの場合は「XCode」->「Open Developer Tool」->「Simulator」で起動。
CUIの場合はopen -a Simulatorで起動できます。

Androidflutter emulatorsで仮想端末を確認できますが、iOSflutter devicesで確認する様子。
Simulatorが起動している状態でflutter runで実行できます。
Androidと同様に「ホットリロードはrを押してね」と表示されるのですが、ターミナルでrを毎回入力する必要があるっぽい?この辺も後々確認していきたいと思います。

iOS環境を整える

終了!と言いたいところなのですが、flutter doctorすると結構エラーが出てきたので、その解決を。

$ flutter doctor

[✓] Flutter (Channel stable, v1.2.1, on Mac OS X 10.14.3 18D109, locale en-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[!] iOS toolchain - develop for iOS devices (Xcode 10.1)
    ✗ libimobiledevice and ideviceinstaller are not installed. To install with Brew, run:
        brew update
        brew install --HEAD usbmuxd
        brew link usbmuxd
        brew install --HEAD libimobiledevice
        brew install ideviceinstaller
    ✗ ios-deploy not installed. To install:
        brew install ios-deploy
    ✗ CocoaPods not installed.
        CocoaPods is used to retrieve the iOS platform side's plugin code that responds to your plugin usage on the Dart side.
        Without resolving iOS dependencies with CocoaPods, plugins will not work on iOS.
        For more info, see https://flutter.io/platform-plugins
      To install:
        brew install cocoapods
        pod setup
[!] Android Studio (version 3.3)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[✓] VS Code (version 1.32.1)
[✓] Connected device (1 available)

Android StudioではなくVS Codeで開発していくのでAndroid Stuidoのエラーは放置。
iOS周りの問題を解決してゆきます。

とはいえflutterがコマンドを教えてくれているので、それをひとつひとつ実行するだけ。

# libimobiledevice and ideviceinstaller are not installed.
$ brew update
$ brew install --HEAD usbmuxd
$ brew link usbmuxd
$ brew install --HEAD libimobiledevice
$ brew install ideviceinstaller

# ios-deploy not installed.
$ brew install ios-deploy

# CocoaPods not installed.
$ brew install cocoapods
$ pod setup

(CocoaPodsはダウンロードに時間かかります)

全部終わったら再度flutter doctorを実行。

$ flutter doctor

Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, v1.2.1, on Mac OS X 10.14.3 18D109, locale en-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 28.0.3)
[✓] iOS toolchain - develop for iOS devices (Xcode 10.1)
[!] Android Studio (version 3.3)
    ✗ Flutter plugin not installed; this adds Flutter specific functionality.
    ✗ Dart plugin not installed; this adds Dart specific functionality.
[✓] VS Code (version 1.32.1)
[✓] Connected device (1 available)

無事環境構築が完了しました!

はじめてのFlutter〜環境構築前編〜

新しくReactNativeを使う予定だったのですが、予定変更でFlutterでやるよ!ということになったので、Flutterはじめました。
ので、今日はその環境構築メモの前半戦を記載していきたいと思います。

DLに時間がかかるものリスト

先にDLをしかけながら読んでいるとスムーズかもしれません。

Flutterとは?

flutter.dev

Googleが開発している、OSSでモバイルアプリ開発FWでありSDK。開発言語もGoogle開発のDart
ひとつのコードでiOSAndroidの両方に対応した開発ができる。Google開発なのでFirebaseなどのGoogleアプリとの連携しやすさもポイント。
2018年12月に1.0がリリースされたばかりで、まだ新しいFWです!

Flutterの押しポイントは3つ。

  • Fast Development

    without losing state, on emulators, simulators, and hardware for iOS and Android.

保存すると自動でビルドしてくれる機能なのですが、いわゆるホットリロードと違って状態を保ったまま反映してくれる機能。すごい!!

  • Expressive and Flexible UI
    MaterialDesignとiOSのデザイン規範(Cupertino)を踏襲したfull-customizedなデザインを簡単に作れるし、レンダリングもめっちゃ早いよ!ということみたい。

  • Native Performance
    スクロールやナビゲーションなど、iOSAndroidの違いを吸収してくれるので、どちらのプラットフォームでもNativePerfomanceが実現できるよ!ということみたい。

ざっくりFlutterの概要を理解したところで環境構築をしていきます。

FlutterSDKのインストール

わたしの環境はmacOS Mojave 10.14.3です。
System Requirementsは公式サイトにある通り。

公式サイトからSDKをダウンロード。回線によってはやや時間がかかります。

DLが完了したらzipファイルを好きなディレクトリに移して解凍。
ターミナルでそのディレクトリに移動、パスを通します。

pwd
# => /Users/makicamel/dev
export PATH="$PATH:`pwd`/flutter/bin"

これだけだと再起動するとパスが消えるので、永続化するには.bash_profileに書き込みます。

touch ~/.bash_profile
vim ~/.bash_profile

export PATH=$PATH:flutterを置いたPATH
# 例えばこう
# export PATH=$PATH:/Users/makicamel/dev/flutter/bin

.bash_profileは変更しただけでは反映されないのでsourceコマンドで反映します。

source ~/.bash_profile

確認はprintenvでできます。
printenvだけだと環境変数全てが返ってきて読みにくいので、PATHを指定。

printenv PATH

flutter --verionを実行して、Welcome to Flutter!メッセージと共にバージョン情報が返ってきたらSDK設定が完了。

$ flutter --version

Flutter 1.2.1 • channel stable • https://github.com/flutter/flutter.git
Framework • revision 8661d8aecd (3 weeks ago) • 2019-02-14 19:19:53 -0800
Engine • revision 3757390fa4
Tools • Dart 2.1.2 (build 2.1.2-dev.0.0 0a7dcf17eb)

なお、もし必要なパッケージがインストールされていなくてエラーが出たときは、flutter doctorで確認できるそうです。

Android環境を構築

Android Studioにアクセス、「DOWNLOAD ANDROID STUDIO」から をダウンロードします。

900MBほどあるので時間がかかります。

ダウンロードが完了したらApplicationディレクトリにインストールし、設定を進めます。

  • Import Android Studio Settings from
    -> 初回インストールなので「Do not import settings」を選択して起動します。

  • Data Sharing
    -> Android Studioをよりよくする為に、どんな機能を使っているか送信してもいいですか、というやつ。お好みに合わせて。

が、時間切れなので、今日はここまで!

また明日、後編をアップいたします😊

セッションにまつわる言葉について整理してみたお話

Webアプリケーションの開発に携わっていると当たり前のように見聞きする「セッション」ですが、徳丸本輪読会でセッション攻撃の対策をまとめていて、ふと理解があやしいなと思ったので、改めてまとめ直してみました。

セッションとは?

ユーザの状態を覚えておくためにサーバに保存するもの。

HTTPはステートレスなプロトコル。文字通り、状態を保存できない。するとこんなことが起きます。

  1. ユーザがサーバにログインの為のリクエストを出して、サーバがレスポンスを返してログインする
  2. ユーザが再度リクエスト投げる
  3. サーバは『このユーザは5秒前にログインしたユーザですよ』と識別できない
    (ので、もう一度ログイン処理が必要になってしまう)

でもそれだと不便なので、ユーザのデータをハッシュ化して、キー(ID)とセットでサーバに保存する。このキーと値のセットをセッションという

もうひとつの意味としては、ユーザがログインしてからログアウトするまでの一連の操作のこと。

セッションとは何か - Railsセキュリティガイド

セッションIDとは?

セッションのキーと値のセットの内のキーのこと。保存先はDBまたはインメモリ、またはサーバ内のファイル。

ユーザ情報をサーバに保存するだけだと、どうやってユーザとセッションを紐づけたらいいかわからない!😖

なので、このセッションIDをサーバとクライアント(いわゆるブラウザ。Cookieとして保存します)に保存して、リクエストを送信する時にセッションIDを送信し、サーバ側に保管されているセッションIDを引いて値を取り出す、ということをしています。

よく聞く「セッションハイジャック」「セッション固定攻撃」はこのセッションIDを盗みとったり、ユーザに強制したりしてなりすましを行う攻撃です。

セッション変数とは?

セッションのキーと値のセットの内の値のこと。

ユーザ情報を保管する他、突合のためのCSRFtokenを保管したりもします。

セッションCookieとは?

セッションIDを保管するためのCookieのこと。

ブラウザを閉じると破棄されるCookieのことを「セッションCookie」ともいうらしい…?(この辺はまだよくわかっていないです)

なんとなくの理解で使っていた「セッション」「セッションID」など、理解するとRailsのコードも何をしているのかが考えやすくなりました!

※ 調べた上で記載していますが、間違っているところがあったらご指摘頂けると大変幸いです!!

content_tagの便利さと新時代tag

今日はRailsのメソッドを新しく知ったので、そのメモを。

ビューでタグを記載する時、通常content_tagを使います。

content_tag - リファレンス - - Railsドキュメント

使い方はこんな感じでとってもかんたん。

content_tag :p, 'Hello, World!'

ネストにするときは文字列じゃなくてブロックを渡すこともできて読みやすい、書きやすい。

content_tag :tr do
  content_tag :th, @user.name
  content_tag :td, @user.gender
  content_tag :td, @user.country
end

タグを指定する第一引数はシンボルも文字列も受け付けてくれる。htmlオプションもハッシュで渡せます。

content_tag 'span', @book.title, class: @book.status

個人的な一番の使いどころはセキュリティ対策。

RailsはすごいFWで、ふつうにRailに乗っていたらなんでもかんでも対策してくれている。XSSも、SQLインジェクションも、CSRFも。

ただ、その為にはちゃんと「Railsを知る」ことが大事だなあと最近よく感じます。

たとえば、最近あったのはページネーションを自力で書くこと。

翌月末日までのデータを2週間ずつ表示する、みたいな感じで、既存のすばらしいGemは使えなかったので自分で書きました。 ヘルパーで動的に生成していたのだけど、最初はとにかく書くことを目的として生のタグで書くからこんな風に書きがち。(わたしは)

def weeks_pagination
(略)
  "<span>1 | <a href='#{...}'>2</a> ...</span>".html_safe
  # => <span>1 | <a href='index?room_id=1&page=2'>2</a> ...</span>
end

このhtml_safeは、「この文字列は安全なので、htmlとして変換してもいいですよ!」って開発者が太鼓判を押した印。

なのだけど、この時パラメータをうっかりそのまま受け取って表示してしまうと、XSSの原因になる。

# URLに「hoge/index?room_id=1'+onmouseover='javascript:alert(\"hello\")」を指定する
def weeks_pagination
  "<span>1 | <a href='#{...}'>2</a> ...</span>".html_safe
end
# => <span>1 | <a href='index?page=2&room_id=1' onmouseover='javascript:alert(\"hello\")'>2</a> ...</span>
# '2'にカーソルを乗せるとアラートが出ますよ、というXSS

(うまい例が思いつかない…😣)

こんな時のためのcontent_tagRailsがちゃんとエスケープしてくれます。

def weeks_pagination
  content_tag :span do
    1 | link_to ..., 2
  end
end

冷静に考えたらこんなことするわけないのですが、ややこしい処理を書いているときはわたしは視野が狭くなるので、普段から.html_safe使いたくなった時は「何かおかしい!」「content_tagで代用できないの?」「ほんとうに安全なの?」と思うようにしています。

習慣って大事ですね。

さて、前置きがかなり長くなったのですが、本題は#content_tagの新しいメソッド、#tag

Rails: 5.1以降タグヘルパーの#tagの新しい記法は#content_tagより便利 - TechRacho

content_tagはすごく便利なメソッドなのですが、やや長い。 それが、Rubyらしくシンプルに書けるようになったとのこと!

# content_tagだとこんな感じ
content_tag :span, 'hello', class: 'greeting'
# シンプル!
tag.span 'hello', class: 'greeting'

# しかも、よくあるこんなミス😣も
content_tag :span, class: 'bookmark'
# => <span>{:class=&gt;&quot;bookmark&quot;}</span>
# 心配ご無用、そう、tagメソッドならね!
tag.span class: 'bookmark'

# もちろんネストも書けるし、ブロックも渡せます😏
tag.div tag.p 'nested'
tag.p do
  I'm a block!
end

影響範囲が大きい為まだ非推奨にはなっていないけれど、content_tagは将来的には廃止予定だそうなので、頃合いを見て直していきたいと思います!

rails/tag_helper.rb at 9c35bf2a6a27431c6aa283db781c19f61c5155be · rails/rails · GitHub

protect_from_forgeryに関するRailsのコメントが秀逸すぎたのでぜひみんな読んでほしい

2019年に入ってから、安全なWebアプリケーションの作り方の輪読会に参加させて頂いています。
知っているつもりで知らなかったお話も多く、また人と話すことで深掘りができて、コーディング中もヒヤリ・ハットが少しできるようになってきました。
(ただ、自分の目だけだと見落としがちなので、ツールやレビューによるチェックが1番だと思っています)

先日はCSRF(CrossSiteRequestForgery)対策についてまとめていました。
RailsCSRF対策といえば、ApplicationControllerに書いてあるprotect_from_forgery

どんなことしてるんだろう、とふと気になったのでコードを読んでいたのですが、そこで驚いたのが、コメントの秀逸さ。

rails/request_forgery_protection.rb at master · rails/rails · GitHub

約50行に渡ってコメントが記載されているのですが、CSRFからどうやってアプリを守るか、Railsの方針は何かが平易でわかりやすい言葉で書かれています。

その明確さ、方針のきれいさに読んでいて感動したので、THE・意訳をしてみました。

レンダリングされるHTMLにtokenをふくめることでControllerは守られてるよ

  • このtokenは攻撃者には知られないしランダムな文字列だよ
  • Railsはセッションに保存したtokenとリクエストのtokenがあってるか比較してるよ
  • GETリクエスト(べき等であるべきリクエスト)は保護しないよ
  • (だから大事な処理はぜったいPOSTにして(GETにしないで)ね)
  • でもJavaScriptHTMLのリクエストも含めて、セッションに基づく全リクエストはCSRFで保護されるべきだよ

GETリクエストは保護しない(だって、大事な処理はGETに書かないって決めたから)けど、 罠サイトが<script>タグをつかってあなたのアプリのJavaScriptを呼び出して、データ漏洩しちゃうかもしれない。
だから、XmlHttpRequest (XHR とか Ajaxっていうよね)だけがJavaScriptによるGETリクエストに答えられるようにしてるよ。

XMLHttpRequestとは
スキーム,ドメイン,ポート(これをまとめてオリジンと呼びます)すべてが一致したURLとしか通信できない(リクエストのこと)

そのtokenの埋め込みはどうしているのかというと、これもまた明確に記載されています。

# application.html.erb
<head>
  <%= csrf_meta_tags %>
</head>

# For AJAX requests other than GETs, extract the "csrf-token" from the meta-tag
# and send as the "X-CSRF-Token" HTTP header.
# If you are using jQuery with jquery-rails this happens automatically.

# 超意訳・再び
# GET以外のAjaxリクエストの場合はmetaタグから 'csrf-token' を抽出して
# 'X-CSRF-Token' HTTPヘッダーとして送信するよ
# jquery-rails(ライブラリ)でjQueryを使用してたら自動でやっとくよ〜😇

最後に、APICSRF対策ってどうするんだろう?やっぱりクライアント側でするんだよね…?と気になったのですが、これもまた明記されていました。

# We may want to disable CSRF protection for APIs since they are typically
# designed to be state-less. That is, the request API client will handle
# the session for you instead of Rails.

# APIのCSRFprotectionは無効にしたいよね~
# だってAPIって基本ステートレスだよね
# つまり、RailsじゃなくてリクエストAPIクライアントがセッションを処理するんだよ!

Railsの哲学に触れて感動したお話でした!

Railsのparamsの入力値検証・文字列加工方法について考えてみたお話

 Railsアプリケーションでよくあるparamsの処理。
save前はModelの中でvalid?すればよいですが、オブジェクトに入れる前に入力値検証や文字列の加工をしたかったり、
そもそも検索画面など、保存しないけど値を加工したりとか、よくあるパターンだと思います。
流れでControllerの中に書いていましたが、違うなあ…ともやもやしていました。

class BookingsController < ApplicationController
  def create
    @booking = current_user.bookings.new(processed_params)
    if @booking.save
      # 成功処理
    else
      # 失敗処理
    end
  end
  
  private
  def processed_params
    attrs = booking_params.to_h
    attrs[:room_id] = attrs[:room_id].to_i
    # date_selectを使ったとする
    booking_dates = ['booking_date(1i)', 'booking_date(2i)', 'booking_date(3i)'].map{|key| attrs[key].to_i}
    attrs[:booking_date] = Date.new(*booking_dates) rescue nil
    attrs
  end
  
  def booking_params
    params.require(:bookings).permit(:booking_date, :room_id)
  end
end

 

単純にModel側に移せばいいかというと、さすがに微妙…。

class Booking < ApplicationRecord
  def initialize(data)
    data = process_data(data.to_h)
    super
  end

  private
  def process_data(params)
    # 処理は先程と同様
  end
end

class BookingsController < ApplicationController
  def create
    @bookiing = current_user.bookings.new(booking_params)
    
    if @booking.save
      # 成功処理
    else
      # 失敗処理
    end
  end
  (略)
end

 

Concernに移すのはかなりありな気がする。
ただ、Concernって共通化が目的な印象なので、繰り返し使うとはいえひとつずつのModelに対してこれを書くのも違和感を感じる。

module BookingParameter
  extend ActiveSupport::Concern

  private
  def processed_data(params)
    # 処理は先程と同様
  end
end

class BookingsController < ApplicationController
  include BookingParameter

  def create
    @bookiing = current_user.bookings.new(processed_data(booking_params))
    
    if @booking.save
      # 成功処理
    else
      # 失敗処理
    end
  end
  (略)
end

 

神速さんが「個人的なリファクタリング原則で『引数が1つのメソッドは、その引数のインスタンスメソッドに書き換えられる』がある」とおっしゃっていたけれど、HashやActionContrller::Parametersにモンキーパッチをあてるのはいくらなんでも意味が違うと思う。

class Hash
  def processed_booking_data
    # 処理は先程と同様
  end
end

 

何がいいんだろう…!と唸りながら調べていたら「引数オブジェクト」という単語が目に飛び込んできて、
そうだjokerさんが前におっしゃってた…!と思ったら、まさしく回答者がjokerさんでした。

rails で params に対して複雑な処理をするときのベストプラクティスは? - QA@IT

こんな感じ…?

class BookingParameter
  attr_reader :booking_date, :room_id

  def initialize(attrs = {})
    attrs.assert_valid_keys('day(1i)', 'day(2i)', 'day(3i)', 'room_id')
    booking_dates = ['booking_date(1i)', 'booking_date(2i)', 'booking_date(3i)'].map{|key| attrs[key].to_i}
    @booking_date = Date.new(*booking_dates) rescue nil
@room_id = attrs['room_id'].to_i freeze end def to_h {booking_date: @booking_date, room_id: @room_id} end end class BookingsController < ApplicationController def create @booking = current_user.bookings.new(BookingParameter.new(booking_params.to_h).to_h) if @booking.save # 成功処理 else # 失敗処理 end end (略) end

BookingParameter.new(booking_params.to_h).to_h というのがしっくりこないし
よりよい書き方があると思うのですが、今時点での結論はこんな感じです!