开发者

Ruby on Rails form page caching including authenticity_token

开发者_JS百科I have a simple Ruby on Rails form which includes an authenticity_token. Unfortunatly, I missed that when you page cache this page then the Authenticity Token becomes invalid. I'm glad I figured it out however.

How do you solve caching in such a case?


As Matchu posted, you could implement point two from this post (same link he posted, but found via my Googling as well). This adds a dependency on JavaScript, which may or may not be something you want.

Alternatively, you could look into Fragment Caching. This allows you to cache certain portions of a page, but still generate the dynamic portions (such as forms with authenticity tokens). Using this technique, you could cache the rest of the page, but generate a new form for every request.

One final solution (but the least favourable), is to disable the authenticity token for that specific action. You can do this by adding the following to the beginning of the controller generating that form:

protect_from_forgery :except => [:your_action]

You can also turn off protect_from_forgery for the entire controller by adding the following to the beginning:

skip_before_filter :verify_authenticity_token


It doesn't seem to be a well-solved problem. Point two on this blog post describes how to accomplish the task by using jQuery, but that introduces a Javascript dependency. Weigh your options, I suppose.


You could render a custom tag in the cached markup and replace it with the form rendered on every request.

module CacheHelper
  # Our FORM is deeply nested in the CACHED_PARTIAl, which we
  # cache. It must be rendered on every request because of its
  # authenticity_token by protect_from_forgery. Instead of splitting up the
  # cache in multiple fragments, we replace a special tag with the custom
  # form.
  def cache_with_bla_form(resource, &block)
    form = nil
    doc = Nokogiri::HTML::DocumentFragment.parse( capture { cache("your_cache_key",&block) } )
    doc.css('uncachable_form').each do |element|
      form ||= render(:partial => 'uncachable_form', :resource => resource)
      element.replace form
    end
    doc.to_html
  end
end

And in your view, you just render an empty uncachable_form tag.

<%- cache_with_bla_form resource do %>
  # cachable stuff..
  <uncachable_form />
  # more cachable stuff
<%- end %>

Yes, this can be considered as a Hack, but it won't loosen forgery protection, needs no JS, and decrease the performance gain from caching just a bit. I think someone implemented a similar pattern as a Rack Middleware.


I followed Niklas Hofer's general solution, but I found that his implementation did not match the exact semantics of the Rails cache helper. Namely, it attempted to return the cached HTML from the helper, rather than writing it to the buffer using safe_concat, which is what the Rails helper does.

The Rails helper usage is like this:

- cache do
  = something

Whereas his solution required this syntax:

= cache_with_updated_csrf do
  = something

For consistency, I would prefer that these work the same way. Hence I used this syntax:

- cache_form do
  = something

Here is my implementation. It will also skip caching when caching is disabled, like the Rails helper does.

module CacheHelper
  # Cache a form with a fresh CSRF
  def cache_form(name = {}, options = nil, &block)
    if controller.perform_caching
      fragment = fragment_for(name, options, &block)

      fragment_with_fresh_csrf = Nokogiri::HTML::DocumentFragment.parse( fragment ).tap do |doc|
        doc.css("input[name=#{request_forgery_protection_token}]").each { |e| e['value'] = form_authenticity_token }
      end.to_html

      safe_concat fragment_with_fresh_csrf
    else
      yield
    end

    nil
  end
end


As a more general solution, you could also replace all cached authenticity_tokens with the current ones:

module CacheHelper
  def cache_with_updated_csrf(*a, &block)
    Nokogiri::HTML::DocumentFragment.parse( capture { cache(*a,&block) } ).tap do |doc|
      doc.css("input[name=#{request_forgery_protection_token}]").each { |e| e['value'] = form_authenticity_token }
    end.to_html.html_safe
  end
end

And use = cache_with_updated_csrf do instead of - cache do in your views. Kudos to Bernard Potocki for the idea.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜