在 ajax 表格中包含 Stripe 卡信息会删除 authenticity_token

Including Stripe card info in ajax form removes authenticity_token

我有一个 form_tag 使用 Ajax 通过 Stripe 保存新用户卡。它呈现一个 Stripe 卡片表单,将输入传递给控制器​​方法,然后应该提供一个 js 文件。它在没有 Stripe 的情况下工作,但有了它,我遇到了身份验证问题。

基本代码如下:

<%= form_tag(save_card_path, id:'payment-form', remote: true) do %>
    <div id="card-element">
      <!-- A Stripe Element will be inserted here. -->
    </div>
  <button id="submit-card" class="submit-btn">Save Card</button>
<% end %>

<script type="text/javascript">

  var stripe = Stripe('<%= @stripe_public %>');
  var elements = stripe.elements();

  // Custom styling can be passed to options when creating an Element.
  var style = {
    base: {
      // Add your base input styles here. For example:
      fontSize: '20px',
      color: "#32325d",
    }
  };

  // Create an instance of the card Element.
  var card = elements.create('card', {style: style});

  // Add an instance of the card Element into the `card-element` <div>.
  card.mount('#card-element');

  card.addEventListener('change', function(event) {
    var displayError = document.getElementById('card-errors');
    if (event.error) {
      displayError.textContent = event.error.message;
    } else {
      displayError.textContent = '';
    }
  });

  // Create a token or display an error when the form is submitted.
  var form = document.getElementById('payment-form');
  form.addEventListener('submit', function(event) {
    event.preventDefault();

    stripe.createToken(card).then(function(result) {
      if (result.error) {
        // Inform the customer that there was an error.
        var errorElement = document.getElementById('card-errors');
        errorElement.textContent = result.error.message;
      } else {
        // Send the token to your server.
        stripeTokenHandler(result.token);
      }
    });
  });

  function stripeTokenHandler(token) {
    // Insert the token ID into the form so it gets submitted to the server
    var form = document.getElementById('payment-form');
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', 'stripeToken');
    hiddenInput.setAttribute('value', token.id);
    form.appendChild(hiddenInput);

    // Submit the form
    form.submit();
  }
</script>

card_controller.rb

def save_card
  respond_to do |format|
    format.js
  end
end

save_card.js.erb

$("html").hide();

就像我说的,没有 Stripe 代码一切正常,并且参数中存在 authenticity_token,但是我上面写的代码给出了以下错误:

    def handle_unverified_request
      raise ActionController::InvalidAuthenticityToken
    end

只有参数:

{"utf8"=>"✓", "stripeToken"=>"<token>"}

当我将选项 authenticity_token: true 添加到表单时,参数再次包含一个 authenticity_token,但现在当它到达 format.js 行时,我得到错误

ActionController::UnknownFormat

我在尝试通过 Ajax 以表单形式上传文件时曾 运行 遇到过类似的问题,但我发现了 remotipart gem,这就解决了它。但在这种情况下似乎没有帮助。

有谁知道为什么包含 Stripe 字段会摆脱我的 authenticity_token,以及为什么即使使用 authenticity_token,也无法识别 js 格式?


编辑:

Jquery-ujs 通过 //= require jquery_ujs 行包含在我的 application.js 中,<%= csrf_meta_tags %> 行包含在 application.html.erb 中,我的源代码包括行

<meta name="csrf-param" content="authenticity_token" />
<meta name="csrf-token" content="<TOKEN>" />

无论我的ajax调用是否有效,都是如此。

更新:

这是我添加选项 authenticity_token: true:

时的完整日志

Started POST "/save_card" for 127.0.0.1 at 2020-02-20 00:07:04 +0100

Processing by UsersController#save_card as HTML

Parameters: {"utf8"=>"✓", "authenticity_token"=>"[LONG TOKEN]", "post"=>"47", "transaction"=>"bid", "stripeToken"=>"[STRIPE TOKEN]"}

User Load (0.9ms)

SELECT "users".* FROM "users" WHERE "users"."id" = ORDER BY "users"."id" ASC LIMIT [["id", 2], ["LIMIT", 1]]

(0.3ms) BEGIN SQL

(2.3ms) INSERT INTO "cards" ("stripe_customer_id", "brand", "last4", "exp_month", "exp_year", "user_id", "created_at", "updated_at") VALUES (, , , , , , , ) RETURNING "id" [["stripe_customer_id", "[STRIPE TOKEN]"], ["brand", "Visa"], ["last4", "4242"], ["exp_month", "4"], ["exp_year", "2024"], ["user_id", 2], ["created_at", "2020-02-19 23:07:05.246789"], ["updated_at", "2020-02-19 23:07:05.246789"]]

(2.8ms) COMMIT

Completed 406 Not Acceptable in 672ms (ActiveRecord: 6.3ms)

ActionController::UnknownFormat (ActionController::UnknownFormat): app/controllers/users_controller.rb:485:in `save_card' Started GET "/serviceworker.js" for 127.0.0.1 at 2020-02-20 00:07:05 +0100 Started GET "/serviceworker.js" for ::1 at 2020-02-20 00:07:21 +0100

这是我删除 authenticity_token: true:

时的日志

Started POST "/save_card" for 127.0.0.1 at 2020-02-20 00:33:17 +0100

Processing by UsersController#save_card as HTML

Parameters: {"utf8"=>"✓", "post"=>"47", "transaction"=>"bid", "stripeToken"=>"[STRIPE TOKEN]"} Can't verify CSRF token authenticity. Completed 422 Unprocessable Entity in 1ms (ActiveRecord: 0.0ms)

ActionController::InvalidAuthenticityToken (ActionController::InvalidAuthenticityToken): actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:195:in handle_unverified_request' actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:227:in handle_unverified_request' devise (4.7.0) lib/devise/controllers/helpers.rb:255:in handle_unverified_request' actionpack (5.1.7) lib/action_controller/metal/request_forgery_protection.rb:222:in verify_authenticity_token' activesupport (5.1.7) lib/active_support/callbacks.rb:413:in block in make_lambda' activesupport (5.1.7) lib/active_support/callbacks.rb:197:inblock (2 levels) in halting' actionpack (5.1.7) lib/abstract_controller/callbacks.rb:12:in block (2 levels) in <module:Callbacks>' activesupport (5.1.7) lib/active_support/callbacks.rb:198:inblock in halting' activesupport (5.1.7) lib/active_support/callbacks.rb:507:in block in invoke_before' activesupport (5.1.7) lib/active_support/callbacks.rb:507:ineach' activesupport (5.1.7) lib/active_support/callbacks.rb:507:in invoke_before' activesupport (5.1.7) lib/active_support/callbacks.rb:130:inrun_callbacks' actionpack (5.1.7) lib/abstract_controller/callbacks.rb:19:in process_action' actionpack (5.1.7) lib/action_controller/metal/rescue.rb:20:inprocess_action' actionpack (5.1.7) lib/action_controller/metal/instrumentation.rb:32:in block in process_action' activesupport (5.1.7) lib/active_support/notifications.rb:166:inblock in instrument' activesupport (5.1.7) lib/active_support/notifications/instrumenter.rb:21:in instrument' activesupport (5.1.7) lib/active_support/notifications.rb:166:in instrument' actionpack (5.1.7) lib/action_controller/metal/instrumentation.rb:30:in process_action' actionpack (5.1.7) lib/action_controller/metal/params_wrapper.rb:252:inprocess_action' activerecord (5.1.7) lib/active_record/railties/controller_runtime.rb:22:in process_action' actionpack (5.1.7) lib/abstract_controller/base.rb:124:inprocess' actionview (5.1.7) lib/action_view/rendering.rb:30:in process' actionpack (5.1.7) lib/action_controller/metal.rb:189:indispatch' actionpack (5.1.7) lib/action_controller/metal.rb:253:in dispatch' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:49:indispatch' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:31:in serve' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:50:inblock in serve' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:33:in each' actionpack (5.1.7) lib/action_dispatch/journey/router.rb:33:inserve' actionpack (5.1.7) lib/action_dispatch/routing/route_set.rb:844:in call' serviceworker-rails (0.6.0) lib/serviceworker/middleware.rb:35:in call' remotipart (1.4.3) lib/remotipart/middleware.rb:32:in call' warden (1.2.8) lib/warden/manager.rb:36:inblock in call' warden (1.2.8) lib/warden/manager.rb:34:in catch' warden (1.2.8) lib/warden/manager.rb:34:incall' rack (2.0.7) lib/rack/etag.rb:25:in call' rack (2.0.7) lib/rack/conditional_get.rb:38:incall' rack (2.0.7) lib/rack/head.rb:12:in call' rack (2.0.7) lib/rack/session/abstract/id.rb:232:incontext' rack (2.0.7) lib/rack/session/abstract/id.rb:226:in call' actionpack (5.1.7) lib/action_dispatch/middleware/cookies.rb:613:incall' activerecord (5.1.7) lib/active_record/migration.rb:556:in call' actionpack (5.1.7) lib/action_dispatch/middleware/callbacks.rb:26:inblock in call' activesupport (5.1.7) lib/active_support/callbacks.rb:97:in run_callbacks' actionpack (5.1.7) lib/action_dispatch/middleware/callbacks.rb:24:incall' actionpack (5.1.7) lib/action_dispatch/middleware/executor.rb:12:in call' actionpack (5.1.7) lib/action_dispatch/middleware/debug_exceptions.rb:59:incall' web-console (3.7.0) lib/web_console/middleware.rb:135:in call_app' web-console (3.7.0) lib/web_console/middleware.rb:30:inblock in call' web-console (3.7.0) lib/web_console/middleware.rb:20:in catch' web-console (3.7.0) lib/web_console/middleware.rb:20:incall' actionpack (5.1.7) lib/action_dispatch/middleware/show_exceptions.rb:31:in call' railties (5.1.7) lib/rails/rack/logger.rb:36:incall_app' railties (5.1.7) lib/rails/rack/logger.rb:24:in block in call' activesupport (5.1.7) lib/active_support/tagged_logging.rb:69:inblock in tagged' activesupport (5.1.7) lib/active_support/tagged_logging.rb:26:in tagged' activesupport (5.1.7) lib/active_support/tagged_logging.rb:69:intagged' railties (5.1.7) lib/rails/rack/logger.rb:24:in call' sprockets-rails (3.2.1) lib/sprockets/rails/quiet_assets.rb:13:incall' actionpack (5.1.7) lib/action_dispatch/middleware/remote_ip.rb:79:in call' actionpack (5.1.7) lib/action_dispatch/middleware/request_id.rb:25:incall' rack (2.0.7) lib/rack/method_override.rb:22:in call' rack (2.0.7) lib/rack/runtime.rb:22:incall' activesupport (5.1.7) lib/active_support/cache/strategy/local_cache_middleware.rb:27:in call' actionpack (5.1.7) lib/action_dispatch/middleware/executor.rb:12:incall' actionpack (5.1.7) lib/action_dispatch/middleware/static.rb:125:in call' rack (2.0.7) lib/rack/sendfile.rb:111:incall' railties (5.1.7) lib/rails/engine.rb:522:in call' puma (3.12.1) lib/puma/configuration.rb:227:incall' puma (3.12.1) lib/puma/server.rb:660:in handle_request' puma (3.12.1) lib/puma/server.rb:474:inprocess_client' puma (3.12.1) lib/puma/server.rb:334:in block in run' puma (3.12.1) lib/puma/thread_pool.rb:135:inblock in spawn_thread' Started GET "/serviceworker.js" for 127.0.0.1 at 2020-02-20 00:33:18 +0100

你能试试下面的方法吗,

将选项 authenticity_token: true 添加到表单,然后参数再次包含 authenticity_token。并将您的 save_card.js 重命名为 save_card.js.erb.

保持原样,只更新.submit()

实际问题是在 form.submit() 上作为 HTML 而不是作为 JS 发送。

https://github.com/rails/rails/issues/29546

 //from
 form.submit();

 //to
 form.dispatchEvent(new Event('submit', {bubbles: true})); 

已更新

之前,您正在提交,然后要求再次提交。单击按钮,获取条带数据,然后提交我的答案。以下示例。

此外,您需要为点击提交按钮添加 disable/enable,以忽略多次点击。

...
// Create a token or display an error when the form is submitted.
  let submitCardBtn = document.getElementById('submit-card');

  submitCardBtn.addEventListener('click', function(event) {
    event.preventDefault();

   // disable button
    submitCardBtn.disabled = true;


    stripe.createToken(card).then(function(result) {
      if (result.error) {
        // Inform the customer that there was an error.
        var errorElement = document.getElementById('card-errors');
        errorElement.textContent = result.error.message;

        // enable button on false
        submitCardBtn.disabled = false; 
      } else {
        // Send the token to your server.
        stripeTokenHandler(result.token);
      }
    });
  });

  function stripeTokenHandler(token) {
    // Insert the token ID into the form so it gets submitted to the server
    var form = document.getElementById('payment-form');
    var hiddenInput = document.createElement('input');
    hiddenInput.setAttribute('type', 'hidden');
    hiddenInput.setAttribute('name', 'stripeToken');
    hiddenInput.setAttribute('value', token.id);
    form.appendChild(hiddenInput);

    // Submit the form
   form.dispatchEvent(new Event('submit', {bubbles: true})); 

   // enable button
   submitCardBtn.disabled = false;
  }

假设您在 Rails 5.X 并且您的 application.js 文件中有 //= require rails-ujs

而不是 form.submit(),使用 Rails.fire(form, 'submit') 将允许您通过 Ajax

提交表单