質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

ただいまの
回答率

88.92%

【Ajax, Rails, いいね機能】2回連続いいねボタンを押すとInvalidAuthenticityTokenエラーが出てしまう

解決済

回答 4

投稿 編集

  • 評価
  • クリップ 0
  • VIEW 336

naota7118

score 3

前提・実現したいこと

Ruby on Railsで転職用の個人アプリを制作しています。
Ajaxを使っていいね機能を実装中に以下のエラーメッセージが発生しました。

2回以上連続で「いいね」ボタンを押すと下記のエラーメッセージが表示されます。
(1回だけ押した場合は問題なく動きます)

実現したい挙動は、連続で「いいね」ボタンを押しても問題なく非同期通信が行われて、「いいね」→「いいねを取り消す」または「いいねを取り消す」→「いいね」と変更できるようにすることです。

間違っているかもしれませんが、非同期通信ではRailsがデフォルトでやってくれているCSRF対策の適用外となるから、自分でAuthenticityTokenを生成する記述をしなければならないのではないかと考え、
自分でAuthenticityTokenを生成する記述を他の記事を参考に書いてみましたが、同じメッセージが表示されてしまいました。

もしお分かりになる方いましたら、ご教示頂ければ幸いです。

発生している問題・エラーメッセージ

ActionController::InvalidAuthenticityToken

該当のソースコード

jQuery(like.js)
$(function(){

  function buildHTML(like){
    var html = `<form class="button_to" method="post" action="/posts/${like.post_id}/likes/${like.id}">
                  <input type="hidden" name="_method" value="delete">
                  <input type="submit" value="いいねを取り消す">
                </form>
                <div class="likeCounts">
                  いいね数:
                  ${like.counts}
                </div>`
    return html;
  }

  function buildDeleteHTML(like){
    var html = `<form class="button_to" method="post" action="/posts/${like.post_id}/likes">
                  <input type="hidden" name="_method" value="good">
                  <input type="submit" value="いいね">
                </form>
                <div class="likeCounts">
                  いいね数:
                  ${like.counts}
                </div>`
    return html;
  }

  $('.button_to').on('submit', function(e) {
    e.preventDefault();
    $.ajaxPrefilter(function(options, originalOptions, jqXHR) {
      var token;
      if (!options.crossDomain) {
        token = $('meta[name="csrf-token"]').attr('content');
        if (token) {
          return jqXHR.setRequestHeader('X-CSRF-Token', token);
        }
      }
    });
    if($('.button_to').children().is('#like')) {
      var formData = new FormData(this);
      var url = $(this).attr('action')
      $.ajax({
        url: url,
        type: "POST",
        data: formData,
        dataType: 'json',
        processData: false,
        contentType: false
      }) 
      .done(function(data){
        $('.button_to').remove();
        $('.likeCounts').remove();
        var html = buildHTML(data);
        $('.like').append(html);
        console.log('good');
        // $('.form__submit').prop('disabled', false);
      })
      .fail(function(){
        alert('エラー');
      })
    } else {
      var formData = new FormData(this);
      console.log(this);
      var url = $(this).attr('action')
      $.ajax({
        url: url,
        type: "DELETE",
        data: formData,
        dataType: 'json',
        processData: false,
        contentType: false
      }) 
      .done(function(data){
        $('.button_to').remove();
        $('.likeCounts').remove();
        var html = buildDeleteHTML(data);
        $('.like').append(html);
        // $('.form__submit').prop('disabled', false);
      })
      .fail(function(){
        alert('エラー');
      })
    }
  })
})
ルーティング(routes.rb)
Rails.application.routes.draw do
(中略)
  resources :posts do
    resources :comments, only: :create
    resources :likes, only: [:create, :destroy]
  end
(中略)
end
コントローラー(likes_controller.rb)
class LikesController < ApplicationController
  def create
    @like = current_user.likes.create(post_id: params[:post_id])
    if @like.save
      @likeCounts = Like.where(post_id: params[:post_id])
      respond_to do |format|
        format.json
      end
    else
      flash[:alert] = 'エラーが発生しました。'
      redirect_to post_path(@like.post.id)
    end
  end

  def destroy
    @like = Like.find_by(post_id: params[:post_id], user_id: current_user.id)
    @like.destroy
    if @like.destroy
      @likeCounts = Like.where(post_id: params[:post_id])
      respond_to do |format|
        format.json
      end
    else
      flash[:alert] = 'エラーが発生しました。'
      redirect_to post_path(@like.post.id)
    end
  end
end
ビュー(show.html.haml)
.like
  - if user_signed_in?
    - if current_user.already_liked?(@post)     
      .likeIcon
        %i.far.fa-heart.likeIcon-fa-heart.heart
    - else
      .likeIcon
        %i.far.fa-heart.likeIcon-fa-heart
    - if current_user.already_liked?(@post)
      = button_to 'いいねを取り消す', post_like_path(@post), method: :delete, id: 'delete'
    - else
      = button_to 'いいね', post_likes_path(@post), id: 'like'
  .likeCounts
    いいね数:
    = @post.likes.count
JSON(create.json.jbuilder)
json.user_id @like.user_id
json.post_id  @like.post_id
json.id @like.id
json.counts @likeCounts.length
JSON(destroy.json.jbuilder)
json.user_id @like.user_id
json.post_id  @like.post_id
json.id @like.id
json.counts @likeCounts.length

調べてわかったこと

エラー原因を調べる過程で下記のことを知りました。
・application_controller.rbのprotect_from_forgeryメソッドを定義したことにより、Railsが自動的にCSRF対策をしてくれている。
・CSRFとは、Webアプリケーションに存在する脆弱性、もしくはその脆弱性を利用した攻撃方法のことで、掲示板や問い合わせフォームなどを処理するWebアプリケーションが、本来拒否すべき他サイトからのリクエストを受信し処理してしまう。その対策をする必要がある。

・フォーム送信前のページでワンタイムトークンが生成され、フォーム送信ページにhiddenで生成したトークンが埋め込まれる。リクエスト送信先でセッションに格納されたトークンとリクエストとして送られてきたトークンが一致するか確認する。この確認で一致していないというのが今回のエラー?

仮説

AuthenticityTokenが生成されていない?
リロードすれば、「いいね」ボタンを押して問題なく非同期で動くことから、2回目に「いいね」ボタンを押した時はapplication_controller.rbのprotect_from_forgeryメソッドが機能していないのではないかと予想した。
非同期通信ではRailsがデフォルトでやってくれているCSRF対策の適用外となるから、自分でAuthenticityTokenを生成する記述をしなければならない?

試したこと

下記のコードをajaxメソッドの前に追記したが、うまくいかなかった。

$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
  var token;
  if (!options.crossDomain) {
    token = $('meta[name="csrf-token"]').attr('content');
    if (token) {
      return jqXHR.setRequestHeader('X-CSRF-Token', token);
    }
  }
});

補足情報(FW/ツールのバージョンなど)

Ruby 2.5.1
Rails 5.2.4.3

  • 気になる質問をクリップする

    クリップした質問は、後からいつでもマイページで確認できます。

    またクリップした質問に回答があった際、通知やメールを受け取ることができます。

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 4

checkベストアンサー

+1

実装したい機能に対してのアプローチが少し冗長というか頑張りすぎてる気がしますね、、
このへん参考にして作ってみた方がいい気がします

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/07/09 06:43

    >実装したい機能に対してのアプローチが少し冗長というか頑張りすぎてる気がしますね、、
    このアプローチを取ったのは、現在スクールに通っていまして、スクールのAjax実装のページを参考にするとこちらのアプローチに近い感じだったのが理由です。
    でも現役のエンジニアの方からは冗長な印象を受けるというのは、自分にとっては発見でした。
    このActionController::InvalidAuthenticityTokenというエラーさえ解決できればうまくいきそうなので、もう少しこのやり方でやってみようかなと思いますが、いつまでもここで止まっているわけにもいかないので、解決できなければ教えて頂いた記事のアプローチで進めようと思います。そちらのアプローチの方が必要最小限のコード量で済みそうですし。
    長文失礼しました。教えていただきありがとうございます。

    キャンセル

  • 2020/07/09 20:04

    教えて頂いた記事やこちらの記事(https://qiita.com/shiro-kuro/items/f017dce3d199f06d1dcd)を参考にして、無事いいね機能の非同期化を完了させることができました。ありがとうございました!

    キャンセル

+1

あと、

  function buildHTML(like){
    var html = `<form class="button_to" method="post" action="/posts/${like.post_id}/likes/${like.id}">
                  <input type="hidden" name="_method" value="delete">
                  <input type="submit" value="いいねを取り消す">
                </form>
                <div class="likeCounts">
                  いいね数:
                  ${like.counts}
                </div>`
    return html;
  }

  function buildDeleteHTML(like){
    var html = `<form class="button_to" method="post" action="/posts/${like.post_id}/likes">
                  <input type="hidden" name="_method" value="good">
                  <input type="submit" value="いいね">
                </form>
                <div class="likeCounts">
                  いいね数:
                  ${like.counts}
                </div>`
    return html;
  }


上記のjsで追加するhtmlにid="like"がないと元々のビューにあるhtmlと違ってしまうかと思います。
また、value="good"goodの部分はhttpメソッドを入れるところだと思います。この場合はpostでしょうか。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/07/08 18:37

    返信が大変遅れまして申し訳ございません。INPUT type="hidden"の時のvalue属性にはhttpメソッドが入るということを知りませんでした。大変勉強になりました。ありがとうございます。教えて頂いた箇所を修正いたしました。ちなみにエラーは今も変わらず表示されたままです。

    キャンセル

  • 2020/07/09 20:05

    その後無事いいね機能の非同期化を完了させることができました。自分では気付かない点を教えていただきありがとうございました!

    キャンセル

+1

jbuilderの記述は

json.extract! @like, :user_id, :post_id, :id
json.counts @likeCounts.length


の方が短くて綺麗な気がします。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/07/09 06:35

    jbuilderの書き方でextract!メソッドというのがあることを初めて知りました。教えていただきありがとうございます。

    キャンセル

0

エラーに直接関係ないかもしれませんが、コントローラのdestroyアクションで@likeを2回消そうとしているのが気になりました。

@like.destroy
  if @like.destroy

投稿

編集

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

  • 2020/07/07 09:25

    gohsan53さん
    ご回答頂きありがとうございます。
    自分では気づきませんでしたが、createアクションを見ると@like.saveは1回しか書いていなくてもsaveできているため、@like.destroyも教えて頂いた通り条件分岐の方だけでよく、1つ目は不要かもしれません。
    帰宅しましたらすぐ修正いたします。
    帰宅後の返信となるため、返信が遅くなりますことご容赦ください。

    キャンセル

15分調べてもわからないことは、teratailで質問しよう!

  • ただいまの回答率 88.92%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る

  • トップ
  • Rubyに関する質問
  • 【Ajax, Rails, いいね機能】2回連続いいねボタンを押すとInvalidAuthenticityTokenエラーが出てしまう