Ruby on Rails表单页面缓存包括authenticity_token

时间:2021-12-12 23:17:57

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.

我有一个简单的Ruby on Rails表单,其中包含一个authenticity_token。不幸的是,我没有注意到,当您页面缓存这个页面时,真实性令牌就会变得无效。不过我很高兴我弄明白了。

How do you solve caching in such a case?

在这种情况下,如何解决缓存?

5 个解决方案

#1


22  

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.

正如Matchu所言,你可以从这篇文章中实现第二点(他也发布了同样的链接,但也可以通过我的google搜索找到)。这增加了对JavaScript的依赖,这可能是您想要的,也可能不是。

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:

您还可以通过在开头添加以下内容来关闭整个控制器的protect_from_forgery:

skip_before_filter :verify_authenticity_token

#2


1  

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.

这似乎不是一个很好解决的问题。这篇博客文章的第二点描述了如何使用jQuery完成任务,但是这引入了Javascript依赖项。权衡一下你的选择,我想。

#3


1  

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.

在您的视图中,只呈现一个空的uncachable_form标记。

<%- 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.

是的,这可以被认为是一种攻击,但是它不会放松伪造保护,不需要JS,并且只会降低一点缓存带来的性能收益。我认为有人实现了类似的模式作为机架中间件。

#4


1  

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.

我遵循了Niklas Hofer的通用解决方案,但是我发现他的实现与Rails缓存助手的确切语义不匹配。也就是说,它试图从helper返回缓存的HTML,而不是使用safe_concat将其写入缓冲区,这正是Rails helper所做的。

The Rails helper usage is like this:

Rails助手的用法如下:

- 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.

这是我的实现。当禁用缓存时,它也将跳过缓存,就像Rails助手所做的那样。

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

#5


0  

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

作为一种更通用的解决方案,您还可以将所有缓存的authenticity_token替换为当前的authenticity_token:

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.

在视图中使用= cache_with_updated_csrf而不是- cache do。对Bernard Potocki的看法。

#1


22  

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.

正如Matchu所言,你可以从这篇文章中实现第二点(他也发布了同样的链接,但也可以通过我的google搜索找到)。这增加了对JavaScript的依赖,这可能是您想要的,也可能不是。

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:

您还可以通过在开头添加以下内容来关闭整个控制器的protect_from_forgery:

skip_before_filter :verify_authenticity_token

#2


1  

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.

这似乎不是一个很好解决的问题。这篇博客文章的第二点描述了如何使用jQuery完成任务,但是这引入了Javascript依赖项。权衡一下你的选择,我想。

#3


1  

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.

在您的视图中,只呈现一个空的uncachable_form标记。

<%- 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.

是的,这可以被认为是一种攻击,但是它不会放松伪造保护,不需要JS,并且只会降低一点缓存带来的性能收益。我认为有人实现了类似的模式作为机架中间件。

#4


1  

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.

我遵循了Niklas Hofer的通用解决方案,但是我发现他的实现与Rails缓存助手的确切语义不匹配。也就是说,它试图从helper返回缓存的HTML,而不是使用safe_concat将其写入缓冲区,这正是Rails helper所做的。

The Rails helper usage is like this:

Rails助手的用法如下:

- 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.

这是我的实现。当禁用缓存时,它也将跳过缓存,就像Rails助手所做的那样。

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

#5


0  

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

作为一种更通用的解决方案,您还可以将所有缓存的authenticity_token替换为当前的authenticity_token:

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.

在视图中使用= cache_with_updated_csrf而不是- cache do。对Bernard Potocki的看法。