###前提
####作ろうとしているもの
- RailsでWebアプリを開発中
User has_many Hobbies / Hobby belongs_to User
なHobby
モデルを 1画面(1フォーム)で複数まとめて追加したい
####開発環境
- OS : Ubuntu 14.04.5 64bit(vagrant環境)
- Ruby : 2.3.1
- Rails : 4.2.7.1
$ cat /etc/lsb-release DISTRIB_ID=Ubuntu DISTRIB_RELEASE=14.04 DISTRIB_CODENAME=trusty DISTRIB_DESCRIPTION="Ubuntu 14.04.5 LTS" $ uname -a Linux vagrant-ubuntu-trusty-64 3.13.0-100-generic #147-Ubuntu SMP Tue Oct 18 16:48:51 UTC 2016 x86_64 x86_64 x86_64 GNU/Linux $ ruby -v ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux] $ rails -v Rails 4.2.7.1
####開発経験・スキル
Rails経験は3ヶ月程度、実務(委託や自社サービスという意味)レベルでの経験はなく、Web系の開発経験も無し、個人で開発をしている程度のレベルです。そのレベルの人間の質問なので、おかしなことを言っていれば何でもご指摘頂けると助かります。
###質問内容
今回起きたトラブルに対して色んな調査をしたため、本文がきれいにまとまっておらず申し訳ありません。長文となっていますが、なにかお気付きの点やアドバイスなどあれば、コメント頂けると幸いです。また、teratail初利用なので作法など守れていない点があるかもありません。
2017/02/24 16:36追記
検証に用いたコードをgithubに置いたので、コード中に気になる点などありましたら、ご指摘ください。
https://github.com/17number/rails-multiform-csrf-error
以下、質問本文になります。
User
に紐づく Hobby
を、1画面で3つ追加しようとした。参考にしたページは以下。
Rails4、fields_forを使って一括更新する処理のベストプラクティスは何だろう | もふもふ技術部
具体的なソースコードは以下、あまり関係なさそうな部分は割愛。
ruby
1#app/models/user.rb 2class User < ActiveRecord::Base 3 has_many :hobbies 4end 5 6#app/models/hobby.rb 7class Hobby < ActiveRecord::Base 8 belongs_to :user 9end 10 11#app/views/hobbies/add_hobbies.html.slim 12= form_tag commit_user_hobbies_path, method: :put do 13 - @hobbies.each do |hobby| 14 = fields_for "hobbies[]", hobby do |hf| 15 = hf.label :name 16 = hf.text_field :name 17 br 18 .actions = submit_tag 19 20#app/controllers/hobbies_controller.rb 21class HobbiesController < ApplicationController 22 before_action :set_hobby, only: [:show, :edit, :update, :destroy] 23 before_action :set_user, only: [:index, :new, :create, :edit, :update, :add_hobbies, :commit_hobbies] 24 25 # GET /hobbies/multiadd 26 def add_hobbies 27 # 合計3つのHobbiesを追加したい 28 (3 - @user.hobbies.length).times {@user.hobbies.build} 29 @hobbies = @user.hobbies 30 end 31 32 # PUT /hobbies/multiadd 33 def commit_hobbies 34 # そもそも上手く動かないので具体的な処理は未実装 35 binding.pry 36 redirect_to user_hobbies_path(@user) 37 end 38 39 private 40 # Use callbacks to share common setup or constraints between actions. 41 def set_hobby 42 @hobby = Hobby.find(params[:id]) 43 end 44 45 def set_user 46 @user = User.find(params[:user_id]) 47 end 48end 49 50#config/routes.rb 51Rails.application.routes.draw do 52 resources :users do 53 resources :hobbies do 54 collection do 55 get "add_hobbies" => "hobbies#add_hobbies", as: :add, path: :multiadd 56 match "add_hobbies" => "hobbies#commit_hobbies", as: :commit, path: :multiadd, via: [:post, :put] 57 end 58 end 59 end 60end 61 62#db/schema.rb 63ActiveRecord::Schema.define(version: 20170223020954) do 64 create_table "hobbies", force: :cascade do |t| 65 t.integer "user_id", limit: 4 66 t.string "name", limit: 255 67 end 68 add_index "hobbies", ["user_id"], name: "index_hobbies_on_user_id", using: :btree 69 70 create_table "users", force: :cascade do |t| 71 t.string "name", limit: 255 72 end 73 add_foreign_key "hobbies", "users" 74end
####なぜかCSRF対策に引っ掛かる
このフォームを使った時、特定条件においてCSRF対策に引っ掛かる(Can't verify CSRF token authenticity
)。
現状わかっている条件は以下の通り。
- あらかじめ1つ、もしくは2つの
Hobby
が登録されている状態。つまり、見た目的には複数フォームの内、1つか2つのフォームがあらかじめ埋まっている状態。
なので、画面アクセス時の初期状態が以下であれば、CSRF対策には引っ掛かっていない。
- 3つあるフォームが全て空っぽ
- 3つあるフォームが全て埋まっている(あらかじめ3つの
Hobby
が登録されている)
#####CSRF対策に引っ掛かる原因調査
色々と調べた結果、以下のページに辿り着いたのでもう少し調べてみた。
RailsのCSRF対策の仕組みについて - Programming log - Shindo200
Rails4のCSRF対策で「Can't verify CSRF token authenticity」エラー - Qiita
######verified_request?
メソッドはパスできてるか?
verified_request?
メソッドに binding.pry
を仕込んでみたところ、本来ならパスするはずの以下をパスできていない。(※上手くいくケースは当該条件をパスしている事は確認済)
valid_authenticity_token?(session, form_authenticity_param)
form_authenticity_param = nil
となっているのが原因。なお、form_authenticity_param
は params
から Token
を取ってくるだけの模様。
ruby
1# actionpack-4.2.7.1/lib/action_controller/metal/request_forgery_protection.rb 2# The form's authenticity parameter. Override to provide your own. 3def form_authenticity_param 4 params[request_forgery_protection_token] 5end
######Token
はフォームに埋まっているか
そもそも Token
は form_tag
や form_for
を使えば自動で埋められるということで、当該画面のHTMLソースコードを確認したところ、 Token
は埋まっていた。
######protect_from_forgery
を使ってみたが根本解決せず
参考ページを見て、protect_from_forgery :except => [:commit_hobbies]
を追加してみたところ、CSRF対策(Can't verify CSRF token authenticity
)はパスできるようになった。
が、params
の中身がスカスカになってしまっているため、期待動作(3つの Hobbies
を登録する)は実現できなかった。
ruby
1#上手くいくケース(これはAll blank状態からフォーム利用) 2Rails4.2.7.1 Ruby2.3.1 pry(#<HobbiesController>)> params 3=> {"utf8"=>"?", 4 "_method"=>"put", 5 "authenticity_token"=>"E4Yb/VmuFIi+q4+mdcZ4qoTyDBubl0Wjdz9hxTJ/+cHhkrt7cg2U+aim8l3r0CIyYUoMjs1phfwSz8CTALfLFA==", 6 "hobbies"=>[{"name"=>"a"}, {"name"=>"b"}, {"name"=>"c"}], 7 "commit"=>"Save changes", 8 "controller"=>"hobbies", 9 "action"=>"commit_hobbies", 10 "user_id"=>"2"} 11 12#ダメなケース(フォームが1つ埋まっている状態からフォーム利用) 13Rails4.2.7.1 Ruby2.3.1 pry(#<HobbiesController>)> params 14=> {"controller"=>"hobbies", 15 "action"=>"commit_hobbies", 16 "user_id"=>"2"}
####そもそもコンソール上に表示されているParameters
がおかしいっぽい
色々と見ていると、コンソール上の表示からしておかしい。authenticity_token
など色々と足りていない模様。
# 上手くいくケース Started PUT "/users/2/hobbies/multiadd" for 10.0.2.2 at 2017-02-23 12:48:46 +0900 Processing by HobbiesController#commit_hobbies as HTML Parameters: {"utf8"=>"?", "authenticity_token"=>"E4Yb/VmuFIi+q4+mdcZ4qoTyDBubl0Wjdz9hxTJ/+cHhkrt7cg2U+aim8l3r0CIyYUoMjs1phfwSz8CTALfLFA==", "hobbies"=>[{"name"=>"a"}, {"name"=>"b"}, {"name"=>"c"}], "commit"=>"Save changes", "user_id"=>"2"} # ダメなケース(protect_from_forgery の有無に関わらずこんな感じ) Started POST "/users/2/hobbies/multiadd" for 10.0.2.2 at 2017-02-23 12:50:38 +0900 Processing by HobbiesController#commit_hobbies as HTML Parameters: {"user_id"=>"2"} User Load (1.4ms) SELECT `users`.* FROM `users` WHERE `users`.`id` = 2 LIMIT 1
あらためて確認したところ、protect_from_forgery
を追加しようがしまいが、上記のような内容だった。そもそも Token
が含まれていないため、CSRF対策に引っ掛かっていたっぽい。
#####パケットキャプチャ上は正常に見える
じゃあ、Token
はどこで消えてるのかと思い、開発環境上でパケットキャプチャも取得してみた。パケット中には Token
や hobbies
相当のデータも入っている事を確認した。
####その他 試したこと
#####form_tag
の method
変更
色々とググっていく内に form_tag
の method
を変更したら状況変わるかも、と思い試してみました。が、結果としては同じでした。
試したケース
method=post(default)
method=put
####現状わかっていること
Rails内のどこかで何かの条件を満たせずに(or 満たしてしまい)、必要なパラメータ群が吹っ飛んでいるっぽい、というとこまでは分かったが、スキルと時間が足りないのでこの場で質問させてもらっています。
- 上手くいかない原因は何なのか
- どこを変更すれば良い
- そもそもコードが間違っている
など、なにかコメント頂けると助かります。