<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
  <channel>
    <title>chitsaou</title>
    <link>https://ruby-china.org/chitsaou</link>
    <description>一鸭七吃</description>
    <language>en-us</language>
    <item>
      <title>求救： ElasticSearch 对 String field 的自订排序顺序</title>
      <description>&lt;p&gt;有个功能不知道怎么用 ElasticSearch 实现。&lt;/p&gt;

&lt;p&gt;简单描述是这样：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;有一个 Post model，里面有 "state" 这个 field，用状态机表示，有 published, draft, taken_down 等狀态。&lt;/li&gt;
&lt;li&gt;使用了 ElasticSearch 建索引。（利用了 elasticsearch-model 这个 gem）&lt;/li&gt;
&lt;li&gt;前端希望它可以照 state 排序，但排序的顺序是 draft -&amp;gt; taken_down -&amp;gt; published&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;也就是说，希望可以对 ElasticSearch 下指令，让它依 state 排序，但顺序可以自订。&lt;/p&gt;

&lt;p&gt;翻了 ElasticSearch 的文档，有提到 sort by function score，但不明白具体怎么实作，Google 一些关键字也没有我看得懂的（汗&lt;/p&gt;

&lt;p&gt;所以想请教一下有没有人做过类似的功能，怎么实现的？&lt;/p&gt;</description>
      <author>chitsaou</author>
      <pubDate>Fri, 21 Mar 2014 17:07:18 +0800</pubDate>
      <link>https://ruby-china.org/topics/18073</link>
      <guid>https://ruby-china.org/topics/18073</guid>
    </item>
    <item>
      <title>「简单易懂的 OAuth 2.0」演讲 Slides</title>
      <description>&lt;p&gt;今天在 Ruby Tuesday Taipei 分享了「简单易懂的 OAuth 2.0」，前半段是我所知道的 OAuth 2 的世界，从零开始介绍整个通信协定，后半段简单讲解了我如何整合 Doorkeeper + Grape API。&lt;/p&gt;

&lt;p&gt;基本上看完了之后，你可以知道 OAuth 2 整个通讯协定怎么跑，以及如何造一个 OAuth 2 认证服务器。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://speakerdeck.com/chitsaou/jian-dan-yi-dong-de-oauth-2-dot-0" rel="nofollow" target="_blank"&gt;https://speakerdeck.com/chitsaou/jian-dan-yi-dong-de-oauth-2-dot-0&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;300 页慎入…我现在把他视为教材了&lt;/p&gt;

&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/2013/0e784cc42628086eae3ec3e7d9ef3cd6.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;p.s. 本来想要找一个跟通用 Web 开发有关的节点，但找不到，只好先放在 Ruby 了…&lt;/p&gt;</description>
      <author>chitsaou</author>
      <pubDate>Wed, 27 Nov 2013 00:04:20 +0800</pubDate>
      <link>https://ruby-china.org/topics/15825</link>
      <guid>https://ruby-china.org/topics/15825</guid>
    </item>
    <item>
      <title>OAuth 2.0 教程: Grape API 整合 Doorkeeper </title>
      <description>&lt;p&gt;最近我要实作使用 OAuth 2 认证的 API，我先是看了 Spec (RFC 6740、RFC 6750），然后研究了既有的 Rails solution，但因为 API 是用 Grape 盖的，又 Doorkeeper / Rack::OAuth2 / Grape 内建的 OAuth 2 认证全都无法直接拿来用，所以只好自己实现 API 认证这部份。&lt;/p&gt;

&lt;p&gt;我把实现的过程写成了教程： &lt;a href="http://blog.yorkxin.org/posts/2013/10/10/oauth2-tutorial-grape-api-doorkeeper" rel="nofollow" target="_blank"&gt;http://blog.yorkxin.org/posts/2013/10/10/oauth2-tutorial-grape-api-doorkeeper&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;简单来说是这样：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;用 Devise 造 User (Resource Owner) 系统&lt;/li&gt;
&lt;li&gt;用 Grape 造 API (Resource Server)&lt;/li&gt;
&lt;li&gt;用 Doorkeeper 造 OAuth 2 Provider (Authorization Server)&lt;/li&gt;
&lt;li&gt;自己用 Rack::OAuth2 接 Grape 来造出 API 上的 Guard&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;其中第四项我搞最久，希望可以帮后来的同学省到时间 :)&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;应要求全文转贴 :) 不过很抱歉是繁体的，原本想用 OpenCC 转换却不会用它的命令列工具…&lt;/p&gt;

&lt;hr&gt;

&lt;p&gt;這篇文章示範如何使用 OAuth 2 保護 API，其中 API 是用 Grape 造出來的，掛在 Rails 底下。&lt;/p&gt;

&lt;p&gt;整個實作流程會造出這些東西：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Owner&lt;/strong&gt; - 可以授權給第三方 App 的角色，也就是 User。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Server&lt;/strong&gt; - 用來處理與 OAuth 2 授權有關的事務，像是：

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Clients&lt;/strong&gt; - 需要有 Clients (Apps) 的 CRUD。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Access Token&lt;/strong&gt; (Model) - 需要有個 Model 來儲存 Access Token。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Authorization Endpoint&lt;/strong&gt; - 這裡來處理 Auth Code Grant 和 Implicit Grant。&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Token Endpoint&lt;/strong&gt; - 這裡來真正核發 Token。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Resource Server&lt;/strong&gt; - 給 App 存取的地方，也就是 API，一部份需要 Access Token 才能存取的叫做 Protected Resource。 

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Resource Server 上面的 Guard&lt;/strong&gt; - 用途是「保護某些 API，必須要帶 Access Token 才能存取」，俗稱保全。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;本文使用這些套件來實作：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Resource Owner (User) - &lt;strong&gt;&lt;a href="https://github.com/plataformatec/devise" rel="nofollow" target="_blank" title=""&gt;Devise&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Authorization Server (OAuth 2 Provider) - &lt;strong&gt;&lt;a href="https://github.com/applicake/doorkeeper" rel="nofollow" target="_blank" title=""&gt;Doorkeeper&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Resource Server (API) - &lt;strong&gt;&lt;a href="https://github.com/intridea/grape" rel="nofollow" target="_blank" title=""&gt;Grape&lt;/a&gt;&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Guard - 用 &lt;strong&gt;&lt;a href="https://github.com/nov/rack-oauth2" rel="nofollow" target="_blank" title=""&gt;Rack::OAuth2&lt;/a&gt;&lt;/strong&gt; 來整合 Grape&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;因為 Doorkeeper 的 &lt;code&gt;doorkeeper_for&lt;/code&gt; 只能用在 Rails，而 Guard 只是一個 Rack Middleware，所以這裡要自己拼湊。詳情請見先前的文章 &lt;a href="http://blog.yorkxin.org/posts/2013/10/08/oauth2-ruby-and-rails-integration-review" rel="nofollow" target="_blank" title=""&gt;〈Ruby / Rails 的 OAuth 2 整合方案簡單評比〉&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;所有過程我都會放在 &lt;a href="https://github.com/chitsaou/oauth2-api-sample" rel="nofollow" target="_blank" title=""&gt;chitsaou/oauth2-api-sample&lt;/a&gt; 這個 repository，各 step 有對應的 step-x tag，例如 Step 1 完成的結果可以在 step-1 這個 tag 看到。&lt;/p&gt;

&lt;hr&gt;
&lt;h2 id="Step 1: 造 Resource Owner 邏輯 (User)"&gt;Step 1: 造 Resource Owner 邏輯 (User)&lt;/h2&gt;
&lt;p&gt;用 Devise 做。這個應該是 Rails Developer 的基本功，所以不解釋了，請見 &lt;a href="https://github.com/chitsaou/oauth2-api-sample/tree/step-1" rel="nofollow" target="_blank" title=""&gt;step-1 tag&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;可以試著打開 &lt;code&gt;/pages/secret&lt;/code&gt; ，會要求登入。&lt;/p&gt;
&lt;h2 id="Step 2: 造 Resource Server (API)"&gt;Step 2: 造 Resource Server (API)&lt;/h2&gt;
&lt;p&gt;用 Grape 是因為不想要讓 API 經過太多 Rails 的 stack。&lt;/p&gt;

&lt;p&gt;這個不難，而且不是本文的重點，所以直接看官方文件就好了。成品可以看 &lt;a href="https://github.com/chitsaou/oauth2-api-sample/tree/step-2" rel="nofollow" target="_blank" title=""&gt;step-2 tag&lt;/a&gt; 。&lt;/p&gt;
&lt;h2 id="Step 3: 造 Authorization Server (Provider)"&gt;Step 3: 造 Authorization Server (Provider)&lt;/h2&gt;
&lt;p&gt;既然底是 Rails，那麼就直接上 Doorkeeper 就好了。可以看 &lt;a href="https://github.com/chitsaou/oauth2-api-sample/tree/step-3" rel="nofollow" target="_blank" title=""&gt;step-3 tag&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;&lt;a href="http://railscasts.com/episodes/353-oauth-with-doorkeeper" rel="nofollow" target="_blank" title=""&gt;RailsCasts 有 tutorial&lt;/a&gt; ，若有買 Pro 不妨去看看。不過其實照官方文件做也不難：&lt;/p&gt;

&lt;p&gt;安裝 Doorkeeper Gem&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;gem&lt;/span&gt; &lt;span class="s1"&gt;'doorkeeper'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;別忘了跑 &lt;code&gt;bundle install&lt;/code&gt; 。&lt;/p&gt;

&lt;p&gt;然後跑這些來安裝：&lt;/p&gt;

&lt;p&gt;$ rails generate doorkeeper:install
    $ rails generate doorkeeper:migration
    $ rake db:migrate&lt;/p&gt;

&lt;p&gt;再照他文件說的去接 Devise 來認證 Resource Owner：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# 認證 Resource Owner 的方法，直接接 Devise&lt;/span&gt;
&lt;span class="n"&gt;resource_owner_authenticator&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;current_user&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="n"&gt;warden&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;:scope&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="ss"&gt;:user&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;p&gt;Doorkeeper 會建這些 model:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;OauthApplication&lt;/strong&gt; - Clients 的註冊資料庫&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OauthAccessGrant&lt;/strong&gt; - Auth Code 流程第一步產生的 Auth Grants 的資料庫&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OauthAccessToken&lt;/strong&gt; - 真正核發出去的 Access Tokens，包含對應的 Refresh Token（預設關閉）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Doorkeeper 開的 Routes 有這些：&lt;/p&gt;

&lt;p&gt;| Method (REST) | Path                               | 用途                                |
    |---------------|------------------------------------|------------------------------------|
    | new           | /oauth/authorize                   | Authorization Endpoint             |
    | create        | /oauth/authorize                   | User 許可 Authorization 時的 action |
    | destroy       | /oauth/authorize                   | User 拒絕 Authorization 時的 action |
    | show          | /oauth/authorize/:code             | （應該是用來 Local 測試的）           |
    | update        | /oauth/authorize                   | （不明的 update grant）              |
    | create        | /oauth/token                       | Token Endpoint                      |
    | show          | /oauth/token/info                  | Token Debug Endpoint               |
    | resources     | /oauth/applications                | Clients 管理界面                    |
    | index         | /oauth/authorized_applications     | Resource Owner 管理授權過的 Clients |
    | destroy       | /oauth/authorized_applications/:id | Resource Owner 管理授權過的 Clients |&lt;/p&gt;

&lt;p&gt;其中 Authorization Endpoint 的 show 只會顯示 grant code，可能是 Local Testing 要用的；而 update 則是沒有任何 action 去接它，不確定是不是 dead feature。&lt;/p&gt;

&lt;p&gt;可以發現到：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;幫你蓋好了 Authorization Endpoint 和 Token Endpoint&lt;/li&gt;
&lt;li&gt;還附加 Token Debug Endpoint，在 Implicit Flow 可以驗證 Token 的真實性。&lt;/li&gt;
&lt;li&gt;還有附 Clients 管理界面&lt;/li&gt;
&lt;li&gt;還可以讓 User 管理授權過的 Clients&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;所以一個 Authorization Server 該有的東西它都提供了。&lt;/p&gt;
&lt;h3 id="Step 3.1: 開測試用的 Client"&gt;Step 3.1: 開測試用的 Client&lt;/h3&gt;
&lt;p&gt;蓋完 Authorization Server 之後，要去開一個 Client。可以打開 &lt;code&gt;/oauth/applications&lt;/code&gt; ，其中 Client 的 redierct URI 填入 &lt;code&gt;http://localhost:12345/auth/demo/callback&lt;/code&gt; ，實際上沒有跑 Web server 在 localhost:12345 也沒關係，最終目的是拿到 code 或 token。&lt;/p&gt;

&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/410a127f0e3e7a24017a3545327edbce.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h3 id="Step 3.2: 拿取 Access Token"&gt;Step 3.2: 拿取 Access Token&lt;/h3&gt;
&lt;p&gt;現在可以來試著拿 Access Token 了，我們要用人腦模擬 Client，來跑 &lt;a href="http://blog.yorkxin.org/posts/2013/09/30/oauth2-4-1-auth-code-grant-flow" rel="nofollow" target="_blank" title=""&gt;Authorization Code Grant 的流程&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;步驟如下：&lt;/p&gt;

&lt;p&gt;首先打開剛剛生的 Client 的 show 頁面，會看到有 Application ID、Secret 等資訊的頁面。最下面有一個 Authorize 的連結，點下去會打開到這個網址（假設這個 Rails App 開在 localhost:9999，下同）（中間斷行比較好讀）：&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:9999/oauth/authorize" rel="nofollow" target="_blank"&gt;http://localhost:9999/oauth/authorize&lt;/a&gt;
        ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
        &amp;amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
        &amp;amp;response_type=code&lt;/p&gt;

&lt;p&gt;就像之前在流程文裡介紹過的，它用 GET 去 Authorization Endpoint 求 Grant Code，附上自己的 Client ID 和 Redirection URI。&lt;/p&gt;

&lt;p&gt;接著會問你要 Authorize 還是 Deny，當然選 Authorize。&lt;/p&gt;

&lt;p&gt;接著會跑到一個瀏覽器打不開的網址，是先前設定的 Redirection URI，不過沒關係，我們已經得到 Grant Code 了（中間斷行比較好讀）：&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:12345/auth/demo/callback" rel="nofollow" target="_blank"&gt;http://localhost:12345/auth/demo/callback&lt;/a&gt;
        ?code=21e1c81db4e619a23d4ed46134884104225d4189baa005220bd9b358be8b591a
              ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
              Grant Code   &lt;/p&gt;

&lt;p&gt;如果在 Step 3.1 照網頁上的指示填入 &lt;code&gt;urn:ietf:wg:oauth:2.0:oob&lt;/code&gt; ，最後會出現 grant code 的 show 頁面，直接把 grant code 曬給你，這是 local 用來測試用的，類似 &lt;a href="https://developers.google.com/accounts/docs/OAuth2InstalledApp#choosingredirecturi" rel="nofollow" target="_blank" title=""&gt;Google OAuth 2.0 的流程&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;到這裡，Client 就拿到 Grant Code 了，依照流程，接下來是 Client 要另外從後台偷偷去 Authorization Server 把這個 Grant Code 換成 Access Token。&lt;/p&gt;

&lt;p&gt;因為要填的資料太多了，我抓一下 &lt;a href="http://www.getpostman.com/" rel="nofollow" target="_blank" title=""&gt;Postman&lt;/a&gt; 的畫面，填完最後按下「Send」就會拿到 Access Token 了！&lt;/p&gt;

&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/e2ebd08f77ae98c63c4e38c40618b37d.png" title="" alt=""&gt;&lt;/p&gt;
&lt;h2 id="Step 4: 造 Resource Server 上的 Guard"&gt;Step 4: 造 Resource Server 上的 Guard&lt;/h2&gt;
&lt;p&gt;造 Guard 這件事比較難，就像我&lt;a href="http://blog.yorkxin.org/posts/2013/10/08/oauth2-ruby-and-rails-integration-review" rel="nofollow" target="_blank" title=""&gt;前一篇文章&lt;/a&gt;說過的，在 API 是 Grape 的情況下， &lt;strong&gt;沒有一個 Guard 是可以直接拿來用的&lt;/strong&gt; ；即使你用 Rails 做 API 好了，&lt;code&gt;doorkeeper_for&lt;/code&gt; 現在也還只是半成品。我目前的做法是把 Rack::OAuth2 的 Bearer Token middleware 接到 Grape 上面，邏輯參考了 &lt;code&gt;doorkeeper_for&lt;/code&gt; 的實作方式。&lt;/p&gt;

&lt;p&gt;這裡會寫得比較仔細。可以看 &lt;a href="https://github.com/chitsaou/oauth2-api-sample/tree/step-4" rel="nofollow" target="_blank" title=""&gt;step-4 tag&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;我寫成一個 module，用 &lt;a href="http://api.rubyonrails.org/classes/ActiveSupport/Concern.html" rel="nofollow" target="_blank" title=""&gt;ActiveSupport::Concern&lt;/a&gt; 去簡化 module 化的程式，放在 &lt;code&gt;api/concerns/api_guard.rb&lt;/code&gt;。&lt;/p&gt;
&lt;h3 id="Step 4.1: 安裝 Rack Middleware 來抓取 Access Token (String)"&gt;Step 4.1: 安裝 Rack Middleware 來抓取 Access Token (String)&lt;/h3&gt;
&lt;p&gt;Rack::OAuth2 這個 Rack Middleware 在安裝 (&lt;code&gt;use&lt;/code&gt;) 的時候要傳一個 block，它會去 call，但 call 的條件是「Request 有帶 OAuth 2 Token」這樣才會 call，意思是說：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果 Request 有帶 &lt;code&gt;Authorization: Bearer XXX&lt;/code&gt; 或 &lt;code&gt;?access_token=xxx&lt;/code&gt; 才會 call&lt;/li&gt;
&lt;li&gt;如果 Request 不帶上述的參數，就不會 call，直接 &lt;strong&gt;pass 到下一個 middleware stack&lt;/strong&gt; （！）&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;而且這個 Middleware 在 call 之後其實會把 return value 直接存進 &lt;code&gt;request.env["某個 key"]&lt;/code&gt; 裡面，意思就是說 &lt;strong&gt;「它只是給你 fetch access token 用的」&lt;/strong&gt; ，不能拿來「確認 Access Token 有效並放行 API access」，這件事要在 API 層做。&lt;/p&gt;

&lt;p&gt;那麼就來安裝這個 Middleware 吧，但只拿來 fetch access token string：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;included&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="c1"&gt;# OAuth2 Resource Server Authentication&lt;/span&gt;
  &lt;span class="n"&gt;use&lt;/span&gt; &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Bearer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'The API'&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;request&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
    &lt;span class="c1"&gt;# The authenticator only fetches the raw token string&lt;/span&gt;

    &lt;span class="c1"&gt;# Must yield access token to store it in the env&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;access_token&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;h3 id="Step 4.2: 做一個 private method 來取出先前拿到的 Access Token (String)"&gt;Step 4.2: 做一個 private method 來取出先前拿到的 Access Token (String)&lt;/h3&gt;
&lt;p&gt;前文提到 Middleware 會把 Token 存在 &lt;code&gt;request.env&lt;/code&gt; 裡面，具體就是 &lt;code&gt;request.env[Rack::OAuth2::Server::Resource::ACCESS_TOKEN]&lt;/code&gt; ，所以就把它拿出來吧。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;helpers&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kp"&gt;private&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_token_string&lt;/span&gt;
    &lt;span class="c1"&gt;# The token was stored after the authenticator was invoked.&lt;/span&gt;
    &lt;span class="c1"&gt;# It could be nil. The authenticator does not check its existence.&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;env&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;ACCESS_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;h3 id="Step 4.3: 做一個 private method 來把 Token String 變成 Instance"&gt;Step 4.3: 做一個 private method 來把 Token String 變成 Instance&lt;/h3&gt;
&lt;p&gt;Token String 只是單純的字串，還需要去 Database 裡面撈才能換成 Instance。參考了 &lt;code&gt;doorkeeper_for&lt;/code&gt; 的做法，我直接呼叫它的 &lt;code&gt;AccessToken.authenticate&lt;/code&gt; ，撈不到會直接回 &lt;code&gt;nil&lt;/code&gt;：&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;helpers&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kp"&gt;private&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;find_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_string&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;Doorkeeper&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessToken&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;authenticate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_string&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;h3 id="Step 4.4: 做一個 service 來驗證 Access Token 是否合法"&gt;Step 4.4: 做一個 service 來驗證 Access Token 是否合法&lt;/h3&gt;
&lt;p&gt;OAuth2::AccessTokenValidationService 我放在 app/service 裡面，其中會驗證是否過期 (expired) 、是否被撤銷 (revoked) ，這兩個都是 Doorkeeper::AccessToken 內建的 methods。但此外還要驗證所需的 scopes 是否包含在 Access Token 的 scopes 裡面。回傳的驗證結果會是 &lt;code&gt;VALID&lt;/code&gt; 、 &lt;code&gt;EXPIRED&lt;/code&gt; 、 &lt;code&gt;REVOKED&lt;/code&gt; 、 &lt;code&gt;INSUFFICIENT_SCOPE&lt;/code&gt; 四個常數的其中一個（定義在該 module 裡面）。&lt;/p&gt;

&lt;p&gt;在 Grape Endpoint 放一個 &lt;code&gt;validate_access_token&lt;/code&gt; helper 來方便處理這件事，它會直接回傳結果，也就是上述四個之中的一個，caller 就可以根據驗證結果決定要怎麼回 response。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;helpers&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="kp"&gt;private&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;validate_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessTokenValidationService&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="n"&gt;scopes&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;在 Service 裡面要驗證 scopes，我的演算法其實很簡單，就是集合比較而已；有的網站會有「A scope 包含 B scope」的設計，如果要做成這樣的話，就不能用單純的集合比較了。純集合比較的演算法是這樣：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;如果沒有要求任何 scopes，那其實任何 Access Token 都符合，就回 true。&lt;/li&gt;
&lt;li&gt;如果有要求任何 scopes，那麼「授權過的 scopes」就得是「所需的 scopes」的宇集，剛好 Ruby 有內建 Set 這個資料結構，把兩個 Array 都轉成 Set 就能方便比較了。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="kp"&gt;protected&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sufficent_scope?&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;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;
    &lt;span class="c1"&gt;# if no any scopes required, the scopes of token is sufficient.&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kp"&gt;true&lt;/span&gt;
  &lt;span class="k"&gt;else&lt;/span&gt;
    &lt;span class="c1"&gt;# If there are scopes required, then check whether&lt;/span&gt;
    &lt;span class="c1"&gt;# the set of authorized scopes is a superset of the set of required scopes&lt;/span&gt;
    &lt;span class="n"&gt;required_scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;authorized_scopes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&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="nf"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;authorized_scopes&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;required_scopes&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;h3 id="Step 4.5: 製作 Guard 來擋住沒有合法 Access Token 的 Requests"&gt;Step 4.5: 製作 Guard 來擋住沒有合法 Access Token 的 Requests&lt;/h3&gt;
&lt;p&gt;現在要真正寫 &lt;code&gt;guard!&lt;/code&gt; method 來擋 API use 了。&lt;/p&gt;

&lt;p&gt;為了讓程式流程看起來更簡潔，根據不同的錯誤情況，定義了不同的 Exception，各 Exception 要怎麼處理，則可以交由 Grape 的 &lt;code&gt;rescue_from&lt;/code&gt; 處理（我是這樣做的），或 Exception 裡面直接 raise Rack::OAuth2 內建的 exception。&lt;/p&gt;

&lt;p&gt;邏輯是這樣：&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;先去抓出 Token String

&lt;ul&gt;
&lt;li&gt;如果沒給 Token，表示 Client 不知道要認證，丟 MissingTokenError&lt;/li&gt;
&lt;li&gt;照 spec 是要回 401 但是不給任何錯誤訊息&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果有給 Token 但是資料庫裡面找不到，丟 TokenNotFound

&lt;ul&gt;
&lt;li&gt;照 spec 是要回 401 加上 Invalid Token Error&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;如果找得到 Token 則進一步驗證是否可以用來存取該 API（根據有否過期、被撤銷，如果有要求 scope 的話則再檢查 scope）

&lt;ul&gt;
&lt;li&gt;若驗證結果是 VALID，則把 &lt;code&gt;@current_user&lt;/code&gt; 指定給該 Access Token 綁定的 Resource Owner (User)&lt;/li&gt;
&lt;li&gt;若驗證結果不是 VALID，則丟出相對應的 Exceptions&lt;/li&gt;
&lt;li&gt;照 spec，如果是因為 scope 不足，則是回 403 加上 Insufficient Scope Error，其他情況則是要回 401 加上 Invalid Token Error&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;helpers&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;guard!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;token_string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_token_string&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;token_string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;blank?&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;MissingTokenError&lt;/span&gt;

    &lt;span class="k"&gt;elsif&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;token_string&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;nil?&lt;/span&gt;
      &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;TokenNotFoundError&lt;/span&gt;

    &lt;span class="k"&gt;else&lt;/span&gt;
      &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;validate_access_token&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Oauth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessTokenValidationService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;INSUFFICIENT_SCOPE&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;InsufficientScopeError&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Oauth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessTokenValidationService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;EXPIRED&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;ExpiredError&lt;/span&gt;

      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Oauth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessTokenValidationService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;REVOKED&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="no"&gt;RevokedError&lt;/span&gt;

      &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;Oauth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;AccessTokenValidationService&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;VALID&lt;/span&gt;
        &lt;span class="vi"&gt;@current_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="no"&gt;User&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;access_token&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resource_owner_id&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;span class="k"&gt;end&lt;/span&gt;
&lt;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="Step 4.6: 把 Exception 轉送到 Rack::OAuth2 內建的錯誤回應方式"&gt;Step 4.6: 把 Exception 轉送到 Rack::OAuth2 內建的錯誤回應方式&lt;/h3&gt;
&lt;p&gt;我的做法是用 Grape 的 &lt;code&gt;rescue_from&lt;/code&gt; 去接 exceptions，當然要直接 raise 也可以就是了。&lt;/p&gt;

&lt;p&gt;要注意的是：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Bearer::ErrorMethods 有內建一組 &lt;code&gt;error_description&lt;/code&gt; 的預設值，根據不同的 &lt;code&gt;error&lt;/code&gt; code 去對應

&lt;ul&gt;
&lt;li&gt;但只有在 Rack 的 authenticator 裡面使用相對應的 helper method (如 &lt;code&gt;insufficiet_scope!&lt;/code&gt;) 才會填入&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;直接 call 這個 middleware 則不會自動填入錯誤訊息&lt;/strong&gt; &lt;/li&gt;
&lt;li&gt;所以必須手動填入&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;沒給 Token 要視為「Client 不知道要 Authenticate」

&lt;ul&gt;
&lt;li&gt;所以 &lt;code&gt;error&lt;/code&gt; code 不屬於 Spec 裡面定義的任何一個&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;error_description&lt;/code&gt; 也不需要給。&lt;/li&gt;
&lt;li&gt;使用 Bearer::Unauthorized 回 401&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Token 找不到、過期 (Expired) 、被撤銷 (Revoked) 的 &lt;code&gt;error&lt;/code&gt; code 都是 &lt;code&gt;invalid_token&lt;/code&gt; 

&lt;ul&gt;
&lt;li&gt;其實可以用同一個 &lt;code&gt;error_description&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;我的寫法會把三種情況分別丟不同的 Exception，並填入不同的 &lt;code&gt;error_description&lt;/code&gt; &lt;/li&gt;
&lt;li&gt;你實作的時候可以用同一個，這並不會違反 spec&lt;/li&gt;
&lt;li&gt;使用 Bearer::Unauthorized 回 401&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Token 的 scope 不足會使用 &lt;code&gt;insufficient_scope&lt;/code&gt; 的 &lt;code&gt;error&lt;/code&gt;

&lt;ul&gt;
&lt;li&gt;使用 Bearer::Forbidden 回 403&lt;/li&gt;
&lt;li&gt;可是 Rack::OAuth2 的實作並沒有填入 &lt;code&gt;WWW-Authenticate&lt;/code&gt; header（只有 401 強制要求要填）&lt;/li&gt;
&lt;li&gt;所有的 error message（包括 &lt;code&gt;scope&lt;/code&gt;）會出現在 JSON response body 裡面&lt;/li&gt;
&lt;li&gt;我有&lt;a href="https://github.com/chitsaou/rack-oauth2/tree/scope-error-params" rel="nofollow" target="_blank" title=""&gt;另外實作一個 fork&lt;/a&gt; ，會一併填入 &lt;code&gt;WWW-Authenticate&lt;/code&gt; 裡面&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;這個實作沒有填入 &lt;code&gt;error_uri&lt;/code&gt; 和 &lt;code&gt;realm&lt;/code&gt; ，其 &lt;code&gt;realm&lt;/code&gt; 會使用 Rack::OAuth2 內建的。&lt;/li&gt;
&lt;/ul&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="n"&gt;included&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;base&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
  &lt;span class="n"&gt;install_error_responders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&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;# ... &lt;/span&gt;

&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ClassMethods&lt;/span&gt;
  &lt;span class="kp"&gt;private&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;install_error_responders&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;error_classes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt; &lt;span class="no"&gt;MissingTokenError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;TokenNotFoundError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                      &lt;span class="no"&gt;ExpiredError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;RevokedError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;InsufficientScopeError&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt; &lt;span class="ss"&gt;:rescue_from&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;error_classes&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oauth2_bearer_token_error_handler&lt;/span&gt;
  &lt;span class="k"&gt;end&lt;/span&gt;

  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;oauth2_bearer_token_error_handler&lt;/span&gt;
    &lt;span class="no"&gt;Proc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;
      &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;MissingTokenError&lt;/span&gt;
          &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Bearer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;

        &lt;span class="k"&gt;when&lt;/span&gt; &lt;span class="no"&gt;TokenNotFoundError&lt;/span&gt;
          &lt;span class="no"&gt;Rack&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;OAuth2&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Server&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Resource&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Bearer&lt;/span&gt;&lt;span class="o"&gt;::&lt;/span&gt;&lt;span class="no"&gt;Unauthorized&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="ss"&gt;:invalid_token&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"Bad Access Token."&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# etc. etc.&lt;/span&gt;
        &lt;span class="k"&gt;end&lt;/span&gt;

      &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finish&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;h3 id="Step 4.7: 製作用來擋全 API 的 Guard"&gt;Step 4.7: 製作用來擋全 API 的 Guard&lt;/h3&gt;
&lt;p&gt;這是仿 &lt;code&gt;doorkeeper_for :all&lt;/code&gt; 的，用途是「這個 API 底下的所有 Endpoints 都要擋」。在 Grape 的世界裡面，它要是放在 Grape::API 裡面的 class method，所以我寫在 ClassMethods module 裡面，一 call 就是塞 before filter 進去，它底下每個 endpoint 都會過這個 filter。&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;ClassMethods&lt;/span&gt;
  &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;guard_all!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
    &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;guard!&lt;/span&gt; &lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="n"&gt;scopes&lt;/span&gt;
    &lt;span class="k"&gt;end&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;h3 id="Step 4.8: 現在可以用 OAuth 2 來擋 API 了"&gt;Step 4.8: 現在可以用 OAuth 2 來擋 API 了&lt;/h3&gt;
&lt;p&gt;單獨擋一個 Endpoint:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SampleAPI&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;
    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"secret"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="n"&gt;guard!&lt;/span&gt; &lt;span class="c1"&gt;# Requires a valid OAuth 2 Access Token to use this Endpoint&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:secret&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"only smart guys can see this ;)"&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;擋一個 API 底下所有 Endpoints:&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="k"&gt;module&lt;/span&gt; &lt;span class="nn"&gt;V1&lt;/span&gt;
  &lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SecretAPI&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="no"&gt;Base&lt;/span&gt;
    &lt;span class="n"&gt;guard_all!&lt;/span&gt;  &lt;span class="c1"&gt;# Requires a valid OAuth 2 Access Token to use all Endpoints&lt;/span&gt;

    &lt;span class="n"&gt;get&lt;/span&gt; &lt;span class="s2"&gt;"secret1"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:secret1&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Hi, &lt;/span&gt;&lt;span class="si"&gt;#{&lt;/span&gt;&lt;span class="n"&gt;current_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&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;get&lt;/span&gt; &lt;span class="s2"&gt;"secret2"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:secret2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"only smart guys can see this ;)"&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;span class="k"&gt;end&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;h3 id="試試看！"&gt;試試看！&lt;/h3&gt;
&lt;p&gt;不帶 Token 就去打 API 會被拒絕：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -i http://localhost:9999/api/v1/secret/secret1.json
HTTP/1.1 401 Unauthorized
WWW-Authenticate: Bearer realm="The API"
Content-Type: application/json
Cache-Control: no-cache

{"error":"unauthorized"}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;附 Token 再去打 API 就沒問題了，並且會告訴我這個 User 是誰：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -i http://localhost:9999/api/v1/secret/secret1.json \
&amp;gt; -H "Authorization: Bearer a14bb554309df32fbb6a3bad6cba25f32a28acc931a74ead06ca904c05281b4c"
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=0, private, must-revalidate

{"secret1":"Hi, ducksteven@gmail.com"}
&lt;/code&gt;&lt;/pre&gt;&lt;h2 id="Step 5: 使用 Scope"&gt;Step 5: 使用 Scope&lt;/h2&gt;
&lt;p&gt;到目前為止，實作出來的 OAuth 2 的 Guard 雖然支援「scopes」，但 Authorization Server 不支援。要怎麼限制某些 API 必須使用「授權了某些 scopes」的 Access Token 才能存取呢？以下是範例，詳細可以參考 Doorkeeper 的文件 &lt;em&gt;&lt;a href="https://github.com/applicake/doorkeeper/wiki/Using-Scopes" rel="nofollow" target="_blank" title=""&gt;Using Scopes&lt;/a&gt;&lt;/em&gt; 。程式碼見 &lt;a href="https://github.com/chitsaou/oauth2-api-sample/tree/step-5" rel="nofollow" target="_blank" title=""&gt;step-5 tag&lt;/a&gt; 。&lt;/p&gt;

&lt;p&gt;首先在 &lt;code&gt;config/initializers/doorkeeper.rb&lt;/code&gt; 裡面增加 scopes 的定義，例如&lt;/p&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Define access token scopes for your provider&lt;/span&gt;
&lt;span class="c1"&gt;# For more information go to https://github.com/applicake/doorkeeper/wiki/Using-Scopes&lt;/span&gt;
&lt;span class="n"&gt;default_scopes&lt;/span&gt;  &lt;span class="ss"&gt;:public&lt;/span&gt;              &lt;span class="c1"&gt;# 如果 Client 不索取任何 scopes 則預設使用這組 scopes&lt;/span&gt;
&lt;span class="n"&gt;optional_scopes&lt;/span&gt; &lt;span class="ss"&gt;:top_secret&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# 其他可以額外申請的 scopes&lt;/span&gt;
                &lt;span class="ss"&gt;:el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:psy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:congroo&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;要重開 Rails server 生效。&lt;/p&gt;

&lt;p&gt;接著打開 Authorize 的頁面，點下去的網址不會帶有「想要索取的 scopes」，所以先把網址複製下來，後面手動附上 &lt;code&gt;scope=top_secret&lt;/code&gt; 參數：（中間斷行比較好讀）&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:9999/oauth/authorize" rel="nofollow" target="_blank"&gt;http://localhost:9999/oauth/authorize&lt;/a&gt;
        ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
        &amp;amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
        &amp;amp;response_type=code
        &amp;amp;scope=top_secret&lt;/p&gt;

&lt;p&gt;到這裡會再問你要不要 Authorize，所以你知道了 Authorization Server 會區別帶有不同 scopes 的 grants。Authorize 之後會得到一組 grant code，再照標準流程拿 Token。我這次拿到的 Token 是 &lt;code&gt;5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;現在我在 SampleAPI 裡面新增兩個 endpoint，需要 scopes 才能存取：&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;"top_secret"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;guard!&lt;/span&gt; &lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:top_secret&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:top_secret&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"T0P S3CR37 :p"&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;get&lt;/span&gt; &lt;span class="s2"&gt;"choice_of_sg"&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
  &lt;span class="n"&gt;guard!&lt;/span&gt; &lt;span class="ss"&gt;scopes: &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="ss"&gt;:el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:psy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="ss"&gt;:congroo&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="ss"&gt;:says&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"El. Psy. Congroo."&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;用之前申請過的 Access Token 來打 &lt;code&gt;top_secret&lt;/code&gt; 這個 API 會被拒絕（中間斷行比較好讀）：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -i http://localhost:9999/api/v1/sample/top_secret.json \
&amp;gt; -H "Authorization: Bearer a14bb554309df32fbb6a3bad6cba25f32a28acc931a74ead06ca904c05281b4c"
HTTP/1.1 403 Forbidden
Content-Type: application/json
Cache-Control: no-cache

{
  "error":"insufficient_scope",
  "error_description":"The request requires higher privileges than provided by the access token.",
  "scope":"top_secret"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;若用新拿到的 Token 就會過了：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -i http://localhost:9999/api/v1/sample/top_secret.json \
&amp;gt; -H "Authorization: Bearer 5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd"
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=0, private, must-revalidate

{"top_secret":"T0P S3CR37 :p"}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;然而如果拿這個 Token 去打 &lt;code&gt;choice_of_sg&lt;/code&gt; 就會被拒絕（中間斷行比較好讀）：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ curl -i http://localhost:9999/api/v1/sample/choice_of_sg.json \
&amp;gt; -H "Authorization: Bearer 5d840a4e43049eb1e66367bc788059f9bf16b53f853f3cd4f001e51a5c95abfd"
HTTP/1.1 403 Forbidden
Content-Type: application/json
Cache-Control: no-cache

{
  "error":"insufficient_scope",
  "error_description":"The request requires higher privileges than provided by the access token.",
  "scope":"el psy congroo"
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;當然，因為 scope 不符啊。這時候就要再重新申請一個 Token，照前面說過的流程，要先有一個 Authorize 的 URL:&lt;/p&gt;

&lt;p&gt;&lt;a href="http://localhost:9999/oauth/authorize" rel="nofollow" target="_blank"&gt;http://localhost:9999/oauth/authorize&lt;/a&gt;
        ?client_id=4a407c6a8d3c75e17a5560d0d0e4507c77b047940db6df882c86aaeac2c788d6
        &amp;amp;redirect_uri=http%3A%2F%2Flocalhost%3A12345%2Fauth%2Fdemo%2Fcallback
        &amp;amp;response_type=code
        &amp;amp;scope=el%20psy%20congroo
                 ^^^   ^^^ space&lt;/p&gt;

&lt;p&gt;多重 scope 的情況下，各 scope 之間用空格 &lt;code&gt;%20&lt;/code&gt; 分開。&lt;/p&gt;

&lt;p&gt;最後我拿到的新 Token 是 &lt;code&gt;0b39839282957d8f80c01901c2468ed52341707594897ec9767af392306f1e55&lt;/code&gt; 。再用它去打 &lt;code&gt;choice_of_sg&lt;/code&gt; API 就會回我了：&lt;/p&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;curl -i http://localhost:9999/api/v1/sample/choice_of_sg.json \
-H "Authorization: Bearer 0b39839282957d8f80c01901c2468ed52341707594897ec9767af392306f1e55"
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: max-age=0, private, must-revalidate

{"says":"El. Psy. Congroo."}
&lt;/code&gt;&lt;/pre&gt;
&lt;hr&gt;
&lt;h2 id="結語"&gt;結語&lt;/h2&gt;
&lt;p&gt;以上的 Tutorial 只簡單示範了如何從零建造一個 OAuth 2 Authorization Server 並且用 OAuth 2 Bearer Token 來保護 Grape API。以下這些問題沒有處理：&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;要可以限制「誰才可以開 Clients」 （例如站長，或是有登入的使用者），我想應該是 Doorkeeper 的 &lt;code&gt;admin_authenticator&lt;/code&gt; 和 &lt;code&gt;enable_application_owner&lt;/code&gt; options 。&lt;/li&gt;
&lt;li&gt;沒有示範 Refresh Token&lt;/li&gt;
&lt;li&gt;Doorkeeper 不能設定只要開啟哪些 Grant Flows

&lt;ul&gt;
&lt;li&gt;這我有開一個 fork 出來實作，也貼了 &lt;a href="https://github.com/applicake/doorkeeper/pull/295" rel="nofollow" target="_blank" title=""&gt;Pull Request&lt;/a&gt; ，等他 merge。&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;因為 Guard 是自己寫的，Doorkeeper 的一些功能完全不使用，例如 &lt;code&gt;access_token_methods&lt;/code&gt; （設定 Client 可以用哪些方式向 API 出示 Token）&lt;/li&gt;
&lt;li&gt;承上，也沒有使用到 Doorkeeper 內建的錯誤訊息 i18n 機制&lt;/li&gt;
&lt;li&gt;Guard 的 Error Response 的 "realm" 不同步&lt;/li&gt;
&lt;li&gt;Guard 的 &lt;code&gt;insufficient_scope&lt;/code&gt; Error 沒有把參數放在 &lt;code&gt;WWW-Authenticate&lt;/code&gt; header

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/chitsaou/rack-oauth2/tree/scope-error-params" rel="nofollow" target="_blank" title=""&gt;我的 fork&lt;/a&gt; 有實作這個，還沒開 PR 給原版。&lt;/li&gt;
&lt;li&gt;雖然在 403 放這個 header doesn't make sense，但總覺得還是統一放在這裡比較好啊…&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Scope 的 matching 只用了簡單的集合比較，不適用於某些 A scope 吃 B scope 情況&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;又，寫完的 Guard 並沒有包成 Gem 還是什麼的……之後應該會包一下吧。&lt;/p&gt;

&lt;p&gt;有任何問題（包括指教本文的謬誤）歡迎在下面的留言板提出 :)&lt;/p&gt;</description>
      <author>chitsaou</author>
      <pubDate>Thu, 10 Oct 2013 21:09:09 +0800</pubDate>
      <link>https://ruby-china.org/topics/14656</link>
      <guid>https://ruby-china.org/topics/14656</guid>
    </item>
    <item>
      <title>Git.io 缩址按一下就产生 (Chrome Extension)</title>
      <description>&lt;p&gt;&lt;a href="http://git.io/help" rel="nofollow" target="_blank" title=""&gt;Git.io&lt;/a&gt; 是 Github 官方的缩址服务，当然也就只支持 Github 的网址。它的操作方式是发送一个 POST 请求到 &lt;code&gt;http://git.io&lt;/code&gt; 并附上表单内容为 &lt;code&gt;url=https://github.com/...&lt;/code&gt; ，通过 &lt;code&gt;curl&lt;/code&gt; 的话就是：&lt;/p&gt;

&lt;p&gt;curl -i &lt;a href="http://git.io" rel="nofollow" target="_blank"&gt;http://git.io&lt;/a&gt; -F "url=&lt;a href="https://github.com/.." rel="nofollow" target="_blank"&gt;https://github.com/..&lt;/a&gt;."&lt;/p&gt;

&lt;p&gt;不知道大家有没有常常要做 Git.io 缩址的需求，但我每次一有需求就必须打开终端机再输入以上指令，我又不常用 curl，等于每有需求就得先查文件。&lt;/p&gt;

&lt;p&gt;于是我觉得太麻烦了，就做了一个 Chrome Extension: &lt;a href="https://chrome.google.com/webstore/detail/baceaeopmlhkjbljoiinmbnnmpokgiml" rel="nofollow" target="_blank" title=""&gt;Git.io URL Shortener&lt;/a&gt; ，在浏览 Github 相关网页的时候，网址列后方就会出现一个 git.io 的小图示，按一下就可以产生短址了，再按一下短址就能复制到剪贴簿；不过我还没有把「立即复制到剪贴簿」的选项实作出来。&lt;/p&gt;

&lt;p&gt;&lt;img src="//l.ruby-china.com/photo/99e290d6037e211e78bba6135a12c23a.png" title="" alt=""&gt;&lt;/p&gt;

&lt;p&gt;当然 source code 也是&lt;a href="https://github.com/chitsaou/git-io-shortener" rel="nofollow" target="_blank" title=""&gt;公开的&lt;/a&gt;。&lt;/p&gt;

&lt;p&gt;各位同学有需要的话就来用一下吧，也欢迎大家给我反馈。  &lt;/p&gt;</description>
      <author>chitsaou</author>
      <pubDate>Tue, 17 Apr 2012 01:17:29 +0800</pubDate>
      <link>https://ruby-china.org/topics/2643</link>
      <guid>https://ruby-china.org/topics/2643</guid>
    </item>
  </channel>
</rss>
