<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>heylonj (Jason Heylon)</title>
    <link>https://ruby-china.org/heylonj</link>
    <description></description>
    <language>en-us</language>
    <item>
      <title>深入 Rails 中的 CSRF Protection</title>
      <description>&lt;p&gt;如果你现在在使用 Rails 进行开发，你很可能就正在使用 CSRF protection。它几乎是在&lt;a href="https://github.com/rails/rails/commit/4e3ed5bc44f6cd20c9e353ab63fd24b92a7942be" rel="nofollow" target="_blank" title=""&gt;一开始的时候就有的特性&lt;/a&gt;，而且就像一些其他的 Rails 特性一样，它会使你的生活更加美好，基本上你都不用考虑它具体做了什么。&lt;/p&gt;

&lt;p&gt;简单来说，跨站请求伪造 (Cross-site request forgery，通常缩写为 CSRF) 就是一个未认证用户伪造了一个数据请求并把它发送到服务器，让服务器认为这个请求是发自一个已认证用户。Rails 会生成一个唯一的 token，并且在每一次接到客户端请求时对请求进行身份验证（注：可以简单说验证这个请求是不是在我们的网站上正常发起的），以避免受到 CSRF 攻击。&lt;/p&gt;

&lt;p&gt;最近，我在 Unbounce 工作中有一个功能需要我考虑如何解决客户端 Javascript 的请求。这个过程让我发现我当时对 CSRF 的了解仅仅停留在它的字面意思上。&lt;/p&gt;

&lt;p&gt;我决定深入的看一下 Rails 的源码以了解 Rails 是如何对这种攻击进行防护的。接下来我会分享一下在这次对 Rails 中的 CSRF Protection 探索中所发现的信息。我们将看到 token 是如何在每次服务器相应中被生成的，以及在接下来的请求中如何使用 token 来验证客户端身份的。&lt;/p&gt;
&lt;h2 id="基础知识"&gt;基础知识&lt;/h2&gt;
&lt;p&gt;Rails 中 CSRF 分别存储在两个地方。一个唯一的 token 会被插入到 HTML 中。同时会有一个相同的 token 会被储存在 session cookie 中。当用户发送一个 POST 请求时，会把 CSRF token 从 HTML 中拿出来放到请求中发送到服务器。Rails 会比对 session cookie 中的 token 和 请求中带有的从 HTML 获取的 token 是否匹配。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog.jasonheylon.com/assets/posts/2018-03-08-a-deep-dive-into-csrf-protection-in-rails/rails_csrf_basic.png" title="Rails对两个CSRF进行比对" alt="Rails对两个CSRF进行比对"&gt;&lt;/p&gt;
&lt;h2 id="如何使用"&gt;如何使用&lt;/h2&gt;
&lt;p&gt;作为 Rails 开发者，你什么都不用做就可以使用 CSRF protection. 在 &lt;code&gt;application_controller.rb&lt;/code&gt; 中有以下一行代码，它开启了 Rails 的 CSRF protection：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;protect_from_forgery&lt;/span&gt; &lt;span class="ss"&gt;with: :exception&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然后，在&lt;code&gt;application.html.erb&lt;/code&gt;文件中，你会发现下面这行代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sx"&gt;%= csrf_meta_tags %&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;就是这些，它已经在 Rails 中很久了，而且我们几乎不需要管它。但 Rails 究竟是如何实现的呢？&lt;/p&gt;
&lt;h2 id="生成和加密"&gt;生成和加密&lt;/h2&gt;
&lt;p&gt;我们从 &lt;code&gt;#csrf_meta_tags&lt;/code&gt;开始。这是一个简单的 helper 方法，它会把 CSRF token 插入到 HTML 中。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# actionview/lib/action_view/helpers/csrf_helper.rb&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;csrf_meta_tags&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;protect_against_forgery?&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"csrf-param"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;request_forgery_protection_token&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"meta"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;name: &lt;/span&gt;&lt;span class="s2"&gt;"csrf-token"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;content: &lt;/span&gt;&lt;span class="n"&gt;form_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;html_safe&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 csrf-token 标签就是我们需要关注的，它就是魔法发生的地方。其中&lt;code&gt;#form_authenticity_token&lt;/code&gt;方法就是获取到真正的 token 地方。接下来我们就要进入到 RequestForgeryProtection 这个 module 内部了。激动的时刻开始了。&lt;/p&gt;

&lt;p&gt;RequestForgeryProtection module 处理了所有的与 CSRF 有关的事。其中包括之前我们在 ApplicationController 中看到的最著名的&lt;code&gt;#protect_from_forgery&lt;/code&gt;方法，这个方法会设置一些 hook 来确保 CSRF token 的校验能在每次请求时都会被触发，以及当校验失败时该如何响应。而且它也会关系到 CSRF token 的生成，加密与解析。让我最喜欢的是这个 module 所涉及的范围很小，在这一个文件中，顺着一些 view 的 helper 方法往下看，你会看到 CSRF protection 的整个实现。&lt;/p&gt;

&lt;p&gt;让我们继续深入 CSRF token 直到它被插入到 HTML 中。&lt;code&gt;#form_authenticity_token&lt;/code&gt; 是一个简单的 wrapper 方法，它接受任意的 hash 参数，也包括 session 本身，继续往下会看到&lt;code&gt;#masked_authenticity_token&lt;/code&gt;方法：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# actionpack/lib/action_controller/metal/request_forgery_protection.rb&lt;/span&gt;

&lt;span class="c1"&gt;# Sets the token value for the current session.&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;form_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;form_options: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt;
  &lt;span class="n"&gt;masked_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;form_options: &lt;/span&gt;&lt;span class="n"&gt;form_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# Creates a masked version of the authenticity token that varies&lt;/span&gt;
&lt;span class="c1"&gt;# on each request. The masking is used to mitigate SSL attacks&lt;/span&gt;
&lt;span class="c1"&gt;# like BREACH.&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;masked_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;form_options: &lt;/span&gt;&lt;span class="p"&gt;{})&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="n"&gt;raw_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;per_form_csrf_tokens&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;action&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;method&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="n"&gt;real_csrf_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="n"&gt;one_time_pad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AUTHENTICITY_TOKEN_LENGTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;encrypted_csrf_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xor_byte_strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one_time_pad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="n"&gt;masked_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;one_time_pad&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;encrypted_csrf_token&lt;/span&gt;
  &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;根据&lt;a href="http://edgeguides.rubyonrails.org/upgrading_ruby_on_rails.html#per-form-csrf-tokens" rel="nofollow" target="_blank" title=""&gt;per-form CSRF tokens in Rails5&lt;/a&gt;中的介绍，&lt;code&gt;#masked_authenticity_token&lt;/code&gt; 方法已经变得有点复杂了。因为这次我们的目的更关注原始的“每次请求一个 CSRF token”实现，也就是之前提到的 meta 标签中的那个，所以我们可以先只关注方法中&lt;code&gt;else&lt;/code&gt;的部分，在这个部分中调用了&lt;code&gt;#real_csrf_token&lt;/code&gt;，并把值返回给&lt;code&gt;raw_token&lt;/code&gt;。&lt;/p&gt;

&lt;p&gt;为什么我们需要把&lt;code&gt;session&lt;/code&gt;传入&lt;code&gt;#real_csrf_token&lt;/code&gt;方法中呢？这个方法做了两件事：首先生成一个未被加密处理的 token，然后把这个 token 插入到 session cookie 中：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# actionpack/lib/action_controller/metal/request_forgery_protection.rb&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;real_csrf_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:_csrf_token&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||=&lt;/span&gt; &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;base64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AUTHENTICITY_TOKEN_LENGTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict_decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:_csrf_token&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;还记得这个方法最初是我们在 application 模板中调用的&lt;code&gt;#csrf_meta_tags&lt;/code&gt;吗，这是一个经典的 Rails 魔法 -- 它保证了 session cookie 中的 token 和页面上的 token 总是匹配的，因为每一次生成的 token 都是先储存在 session cookie 中，然后再把存在 session cookie 中的 token 插入到页面中的。&lt;/p&gt;

&lt;p&gt;让我们看一下&lt;code&gt;#masked_authenticity_token&lt;/code&gt;方法的最后部分：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;one_time_pad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;SecureRandom&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;random_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;AUTHENTICITY_TOKEN_LENGTH&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;encrypted_csrf_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;xor_byte_strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one_time_pad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;masked_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;one_time_pad&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;encrypted_csrf_token&lt;/span&gt;
&lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict_encode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;是时候进行一些加密工作了。现在已经把 token 插入到了 session cookie，接下来方法就要解决到怎样把 token 返回到 HTML 的了，这里我们还提前做了些预防（主要是&lt;a href="https://github.com/rails/rails/pull/16570" rel="nofollow" target="_blank" title=""&gt;降低 SSL 漏洞攻击的可能性&lt;/a&gt;，现在我不会涉及到这些）。需要注意的是 session cookie 中的 token 是没有经过我们的加密的，因为从 Rails4 开始 session cookie 默认就会被加密了。&lt;/p&gt;

&lt;p&gt;首先，我们生成了一次性密钥（one-time pad），我们将要使用它来加密 token。&lt;a href="https://en.wikipedia.org/wiki/One-time_pad" rel="nofollow" target="_blank" title=""&gt;一次性密钥&lt;/a&gt;是一种 使用随机生成的密钥来对定长的纯文本进行加密的方法，并且需要这个密钥对已经加密过的消息进行解密操作。之所以它被称作“一次性”密钥，是因为每以个密钥只用于加密一个文本，然后就会被销毁。Rails 就是为每一个新的 CSRF token 都重新生成一个一次性密钥，然后使用它来对 token 进行 XOR 按位异或操作。再将一次性密钥拼接到上一步得到的字符串前面进行混淆，然后使用 Base64 进行编码，这样就可以把加密好的 token 返回到页面中了。&lt;/p&gt;

&lt;p&gt;&lt;img src="https://blog.jasonheylon.com/assets/posts/2018-03-08-a-deep-dive-into-csrf-protection-in-rails/rails_csrf_encrypt.png" title="Rails CSRF 加密过程" alt="Rails CSRF 加密过程"&gt;&lt;/p&gt;

&lt;p&gt;当这些操作完成后，加密好的认证 token 就会返回到堆栈中，回传到 application 模板中：&lt;/p&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"csrf-param"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"authenticity_token"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"csrf-token"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"vtaJFQ38doX0b7wQpp0G3H7aUk9HZQni3jHET4yS8nSJRt85Tr6oH7nroQc01dM+C/dlDwt5xPff5LwyZcggeg=="&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="解析和验证"&gt;解析和验证&lt;/h2&gt;
&lt;p&gt;我们已经看到了 CSRF token 的生成过程，以及它是如何被存入到 HTML 和 cookie 中的，现在该让我们看看 Rails 在收到的请求时是怎样进行验证的。&lt;/p&gt;

&lt;p&gt;当用户在你的站点提交一个表单时，CSRF token 就会与表单数据一起提交 (默认是名为&lt;code&gt;authenticity_token&lt;/code&gt;的参数)，也可以将它放在 HTTP 头&lt;code&gt;X-CSRF-Token&lt;/code&gt;中发送。&lt;/p&gt;

&lt;p&gt;回忆一下在 ApplicationController 中的这行代码：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;protect_from_forgery&lt;/span&gt; &lt;span class="ss"&gt;with: :exception&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其中，&lt;code&gt;#protect_from_forgery&lt;/code&gt;方法会在每一个 controller action 的生命周期中添加 before_action：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;before_action&lt;/span&gt; &lt;span class="ss"&gt;:verify_authenticity_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;options&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;这个 before action 会将 params 或 header 中传来的 CSRF token 与 session cookie 中的 token 进行比较。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# actionpack/lib/action_controller/metal/request_forgery_protection.rb&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verify_authenticity_token&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;verified_request?&lt;/span&gt;
    &lt;span class="c1"&gt;# handle errors ...&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;

&lt;span class="c1"&gt;# ...&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;verified_request?&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="n"&gt;protect_against_forgery?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;head?&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;valid_request_origin?&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="n"&gt;any_authenticity_token_valid?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在一些判断之后（比如我们不需要验证 HEAD 和 GET 的请求），真正的验证发生&lt;code&gt;#any_authenticity_token_valid?&lt;/code&gt;方法中：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;any_authenticity_token_valid?&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="n"&gt;request_authenticity_tokens&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;any?&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="n"&gt;valid_authenticity_token?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;因为 token 可能在请求的 params 或者 header 中，Rails 要求 params 和 header 的 token 中至少有一个与 session cookie 中的 token 相匹配才算通过验证。&lt;/p&gt;

&lt;p&gt;&lt;code&gt;#valid_authenticity_token&lt;/code&gt;是很长的一个方法，但其实它只是把&lt;code&gt;#masked_authenticity_token&lt;/code&gt;方法中的操作反过来做一遍，先对 token 进行解析然后再做校验：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;valid_authenticity_token?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoded_masked_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;begin&lt;/span&gt;
    &lt;span class="n"&gt;masked_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Base64&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strict_decode64&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoded_masked_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;rescue&lt;/span&gt; &lt;span class="no"&gt;ArgumentError&lt;/span&gt; &lt;span class="c1"&gt;# encoded_masked_token is invalid Base64&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;false&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;AUTHENTICITY_TOKEN_LENGTH&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;

  &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;length&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="no"&gt;AUTHENTICITY_TOKEN_LENGTH&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;csrf_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;unmask_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;compare_with_real_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csrf_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt;
      &lt;span class="n"&gt;valid_per_form_csrf_token?&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csrf_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="kp"&gt;false&lt;/span&gt; &lt;span class="c1"&gt;# Token is malformed.&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;首先，我们需要对已经被 Base64 编码过的字符串进行解码，解码后我们得到了被混淆过的 token，然后去掉之前加入的混淆，再将得到的 token 与 session 中的 token 进行对比：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;unmask_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="n"&gt;one_time_pad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="no"&gt;AUTHENTICITY_TOKEN_LENGTH&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;encrypted_csrf_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;masked_token&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;AUTHENTICITY_TOKEN_LENGTH&lt;/span&gt;&lt;span class="o"&gt;..-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="n"&gt;xor_byte_strings&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one_time_pad&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encrypted_csrf_token&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;在&lt;code&gt;#unmask_token&lt;/code&gt;方法中，我们需要把混淆过的 token 拆分为原来的两部分：一次性密钥和加密后的 token。然后再对这两个字符串进行 XOR 操作，最后得到了 Rails 在最开始生成的明文 token。&lt;/p&gt;

&lt;p&gt;最后，在&lt;code&gt;#compare_with_real_token&lt;/code&gt;方法使用 ActiveSupport::SecureUtils 来对两个 token 进行比较：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;compare_with_real_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# :doc:&lt;/span&gt;
  &lt;span class="no"&gt;ActiveSupport&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;SecurityUtils&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;secure_compare&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;real_csrf_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;最后，我们的请求顺利的通过验证啦~&lt;/p&gt;
&lt;h2 id="总结"&gt;总结&lt;/h2&gt;
&lt;p&gt;我之前从来没有对 CSRF 有过这么多的思考，Rails 其实已经帮我们做了很多事，很多时候我们总是会觉得 它“这样就可以工作了”。每一次去探索这些魔法背后的实现过程都是很美妙的。&lt;/p&gt;

&lt;p&gt;我认为 CSRF protection 的实现是一个很好的对代码进行职责拆分的例子，通过创建一个 module 来暴露一个简单的而稳定的公共接口，底层的实现可以随意做一些小修改也不会影响到其余的源代码 -- 而且你可以看到在这些年 Rails 团队为 CSRF protection 添加的一些功能，就比如 per-form tokens。&lt;/p&gt;

&lt;p&gt;在每一次深入 Rails 源码的过程中我都会学到很多。我希望这些也会激励着你，在遇到一些 Rails 魔法的时候，去看一看魔法下面的真实样子吧。&lt;/p&gt;

&lt;p&gt;英文原文：&lt;a href="https://medium.com/rubyinside/a-deep-dive-into-csrf-protection-in-rails-19fa0a42c0ef" rel="nofollow" target="_blank" title=""&gt;A Deep Dive into CSRF Protection in Rails&lt;/a&gt;&lt;/p&gt;</description>
      <author>heylonj</author>
      <pubDate>Fri, 09 Mar 2018 00:38:02 +0800</pubDate>
      <link>https://ruby-china.org/topics/35199</link>
      <guid>https://ruby-china.org/topics/35199</guid>
    </item>
    <item>
      <title>个人项目： slide 分享服务网站欢迎大家使用</title>
      <description>&lt;p&gt;项目地址： &lt;a href="http://www.weslides.com" rel="nofollow" target="_blank" title=""&gt;http://www.weslides.com&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;项目起因&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;秉着“喜欢/学习一种技术就用它做个项目吧”想法做了这么个小项目，也因为知名的 slideshare 被封，以及 speakerdeck 都放在 aws 上，所以也就有了&lt;a href="http://www.weslides.com" rel="nofollow" target="_blank" title=""&gt;WeSlides&lt;/a&gt;这么个项目，也是给大家在国内 slide 分享方面多一个选择吧。这个项目是在 14 年 11 月月底提交的第一次 commit 在 15 年 1 月部署上线，都是在本人下班回家并且不犯懒的时间开发的。项目目前还是有很多不够完善和需要增加的功能，我也会长期维护的。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;网站提供服务？&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;在注册了用户后，就可以上传自己的幻灯片/演讲稿（目前只支持 pdf）到云（fuwu）端 (qi) 上并分享给大家，可以通过链接分享或嵌入到 html 中。
也就是与之前提到的两个知名网站的服务基本相同，由于自己 UI 水平有限，布局也很相似，并且重度使用 semantic-UI 内建样式，大家轻喷，也不反驳山寨的说法。我的目的就是想做一个真正能为人服务的同时让自己成长的东西出来。&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;欢迎使用与感谢&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;最后希望大家如果使用还请注意版权哦，欢迎提交 bug，不要手软，先谢谢大家了。：）&lt;/p&gt;

&lt;p&gt;最后的最后感谢 ruby-china 以及各种 ruby 社区多年来为大家营造了这么好的环境，虽然我入伙较晚但是依然能感受到社区的活力和热情。&lt;/p&gt;</description>
      <author>heylonj</author>
      <pubDate>Sun, 04 Jan 2015 14:51:48 +0800</pubDate>
      <link>https://ruby-china.org/topics/23533</link>
      <guid>https://ruby-china.org/topics/23533</guid>
    </item>
    <item>
      <title>在这种出现 routes 冲突时大家会怎样做呢？</title>
      <description>&lt;p&gt;相信这样的情况大家都遇到过。
routes.rb&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"/:user_name"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;to: &lt;/span&gt;&lt;span class="s2"&gt;"users#show"&lt;/span&gt; 
&lt;span class="n"&gt;resources&lt;/span&gt; &lt;span class="ss"&gt;:categories&lt;/span&gt;
&lt;span class="o"&gt;....&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;如果有人注册了个 user_name 为"categories"时就会出现 routes 冲突。想问一下大家都是如何解决的呢？&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt; 限制一部分用户名的注册，或在 seed 里面把所有冲突用户名先导入，让“/:user_name”优先级最低。&lt;/li&gt;
&lt;li&gt; 改用“/users/:user_name”，恩，url 长了点，不太优雅呀。&lt;/li&gt;
&lt;li&gt; 还有其他的好方法么？&lt;/li&gt;
&lt;/ol&gt;</description>
      <author>heylonj</author>
      <pubDate>Mon, 15 Dec 2014 15:51:26 +0800</pubDate>
      <link>https://ruby-china.org/topics/23208</link>
      <guid>https://ruby-china.org/topics/23208</guid>
    </item>
  </channel>
</rss>
