鍍金池/ 教程/ Java/ 身份認(rèn)證基礎(chǔ)
集成自動化部署
架設(shè) CI 服務(wù)器
探索用戶資源
SSH agent 轉(zhuǎn)發(fā)
使用評論
身份認(rèn)證基礎(chǔ)
管理部署密鑰
準(zhǔn)備開始
傳遞部署
遍歷分頁
整合者的最佳做法
數(shù)據(jù)渲染成圖表

身份認(rèn)證基礎(chǔ)

在這一節(jié),我們將重點講身份認(rèn)證的基礎(chǔ)知識。明確地說,我們將使用Sinatra創(chuàng)建一個 ruby 服務(wù),該服務(wù)將用幾種不同的方式來實現(xiàn)一個應(yīng)用的 web 流程。

你能夠從平臺范例倉庫下載這個工程的完整源代碼。

注冊你的應(yīng)用

首先,你需要 注冊你的應(yīng)用。每一個已注冊的 OAuth 應(yīng)用將被指定一個唯一的 Client ID 和 Client Secret。注意不要共享你的 Client Secret!包括將該字符串提交到你的 repo 中。

你能夠根據(jù)你的喜好任意填寫每一個信息,除了授權(quán)回調(diào) URL。它無疑是配置你的應(yīng)用最重要的部分。它是 Github 在成功認(rèn)證用戶之后返回的回調(diào) URL。

因為我們是運行一個普通的 Sinatra 服務(wù),本地實例的地址被設(shè)置為 http://localhost:4567。所以讓我們將回調(diào) URL 填寫為 http://localhost:4567/callback

接受用戶授權(quán)

現(xiàn)在,讓我們開始編寫我們簡單的服務(wù)。創(chuàng)建名為 server.rb 的文件并且將以下內(nèi)容粘貼到文件中:

    require 'sinatra'
    require 'rest-client'
    require 'json'

    CLIENT_ID = ENV('GH_BASIC_CLIENT_ID']
    CLIENT_SECRET = ENV('GH_BASIC_SECRET_ID']

    get '/' do
      erb :index, :locals => {:client_id => CLIENT_ID}
    end

你的 client ID 和 client secret 密匙來自你的 應(yīng)用配置頁。你絕不應(yīng)該將這些值存儲于 Github 或其他公共區(qū)域。我們建議將它們保存為 環(huán)境變量 ,這也是我們這里所做的。

接下來,在 views/index.erb 中,粘貼以下內(nèi)容:

    <html>
      <head>
      </head>
      <body>
        <p>
          Well, hello there!
        </p>
        <p>
          We're going to now talk to the GitHub API. Ready?
          <a href="https://github.com/login/oauth/authorize?scope=user:email&client_id=<%= client_id %>">Click here</a> to begin!</a>
        </p>
        <p>
          If that link doesn't work, remember to provide your own <a href="/v3/oauth/#web-application-flow">Client ID</a>!
        </p>
      </body>
    </html>

(如果你不熟悉 Sinatra 是如何工作的,我們建議閱讀 Sinatra 指南

同樣的,注意代碼中的 URL 使用 scope 查詢參數(shù)來定義應(yīng)用程序所要求的權(quán)限區(qū)域(scopes)。對于我們的應(yīng)用,我們請求 user:email 權(quán)限區(qū)域來讀取私有 email 地址。

在你的瀏覽器中打開 http://localhost:4567。點擊該鏈接后,你將跳轉(zhuǎn)至 GitHub,并且顯示類似以下對話框:

http://wiki.jikexueyuan.com/project/github-developer-guides/images/oauth_prompt.png" alt="" />

如果你信任你自己,點擊 Authorize App。哇哦, Sinatra 跳出來一個404錯誤。這是怎么回事?

好吧,記得我們指定了一個回調(diào) URL 為 callback 嗎?我們并沒有為它提供路由,所以 GitHub 在驗證 app 之后,不知道把用戶往哪里丟。現(xiàn)在我們來解決這個問題!

提供一個 callback

server.rb 中,加入一個 route 來指明 callback 應(yīng)該做什么:

    get '/callback' do
      # get temporary GitHub code...
      session_code = request.env['rack.request.query_hash']['code']

      # ... and POST it back to GitHub
      result = RestClient.post('https://github.com/login/oauth/access_token',
                              {:client_id => CLIENT_ID,
                               :client_secret => CLIENT_SECRET,
                               :code => session_code},
                               :accept => :json)

      # extract the token and granted scopes
      access_token = JSON.parse(result)['access_token']
    end

在一次成功的 app 授權(quán)認(rèn)證之后, GitHub 提供了一個臨時的 code 值。你將需要將這個值 POST 回 GitHub 以交換一個 access_token 。我們使用rest-client來簡化我們的 GET 和 POST HTTP 請求。注意,你可能永遠(yuǎn)不會通過 REST 來訪問這些 API 。對于一個更加正式的應(yīng)用,你很可能使用一個你選擇的語言所寫的庫。

確認(rèn)被授予的權(quán)限區(qū)域

在此之后,用戶將能夠編輯你請求的權(quán)限區(qū)域,你的應(yīng)用也可能被授予少于你默認(rèn)請求的數(shù)量的權(quán)限區(qū)域。所以,在你使用該 token 進(jìn)行任何請求前,你應(yīng)該確定用戶授予了該 token 哪些權(quán)限區(qū)域。

被授予的權(quán)限區(qū)域被作為交換 token 時返回值的一部分被返回。

    # check if we were granted user:email scope
    scopes = JSON.parse(result)['scope'].split(',')
    has_user_email_scope = scopes.include? 'user:email'

在我們的應(yīng)用中,我們使用 scopes.include? 來檢查我們是否被授予了 user:email 區(qū)域的權(quán)限,我們需要使用該權(quán)限來獲取授權(quán)用戶的私人 email 地址。如果應(yīng)用程序要求更多其他區(qū)域的權(quán)限,我們也可以以同樣的方式檢查。

還有,因為權(quán)限區(qū)域之間有著繼承的關(guān)系,你必須檢查你被授予了所請求的最低級別的權(quán)限。比如說,如果應(yīng)用請求了 user 區(qū)域權(quán)限,但是它可能只被授予了 user:email 區(qū)域權(quán)限。在這種情況下,應(yīng)用將不會被授予它所請求的權(quán)限,但是已經(jīng)被授予的區(qū)域權(quán)限仍然是有效的。

僅在進(jìn)行請求之前檢查區(qū)域授權(quán)情況是不夠的,因為用戶可能在你檢查授權(quán)情況和進(jìn)行實際請求之間改變了區(qū)域權(quán)限。如果這種情況發(fā)生了,原本你預(yù)計成功的請求,可能會失敗并返回一個404401狀態(tài),或者返回一個不同的信息子集。

為了讓你能夠更優(yōu)雅地處理這些情況,所有有效 token 發(fā)起的 API 請求的返回值都包含一個 X-OAuth-Scopes 頭部。這個頭部包含了該 token 用來發(fā)起請求的區(qū)域列表。除此之外,授權(quán) API 還提供了一個終端來檢查一個 token 的有效性。使用這個信息來檢測 token 授權(quán)區(qū)域的改變,并且告知你的用戶可用應(yīng)用功能的改變。

發(fā)起已認(rèn)證請求

最后,使用這個 access token,你將能夠作為一個已登錄用戶發(fā)起已認(rèn)證的請求。

    # fetch user information
    auth_result = JSON.parse(RestClient.get('https://api.github.com/user',
                                            {:params => {:access_token => access_token}}))

    # if the user authorized it, fetch private emails
    if has_user_email_scope
      auth_result['private_emails'] =
        JSON.parse(RestClient.get('https://api.github.com/user/emails',
                                  {:params => {:access_token => access_token}}))

    erb :basic, :locals => auth_result

我們能夠使用我們的結(jié)果做任何我們想要的事。在這里,我們僅僅是將它們直接輸出到 basic.erb 中:

    <p>Hello, <%= login %>!</p>
    <p>
      <% if !email.nil? && !email.empty? %> It looks like your public email address is <%= email %>.
      <% else %> It looks like you don't have a public email. That's cool.
      <% end %>
    </p>
    <p>
      <% if defined? private_emails %>
      With your permission, we were also able to dig up your private email addresses:
      <%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
      <% else %>
      Also, you're a bit secretive about your private email addresses.
      <% end %>
    </p>

實現(xiàn)持久授權(quán)

如果我們要求用戶每次進(jìn)入網(wǎng)頁的時候都需要登錄 app ,那是非常糟糕的。例如,嘗試直接打 http://localhost:4567/basic。你將看到一個報錯。

假如我們能夠跳過整個“點擊這里”的過程,而是僅僅記住它,只要用戶登錄了 GitHub,它們就能夠使用這個應(yīng)用,那會怎樣?請保持淡定,因為這就是接下來我們要做的。

我們上面縮寫的小服務(wù)器是非常簡單的。為了能夠嵌入一些智能的認(rèn)證機(jī)制,我們將轉(zhuǎn)而使用回話來保存 token。這將使得認(rèn)證對用戶來說是透明的。

另外,因為我們要在會話中保持授權(quán)區(qū)域,我們需要處理用戶在我們檢查之后更新了區(qū)域,或者撤消了標(biāo)識的情況。為了做到這一點,我們將使用一個 rescue 區(qū)塊并檢查第一個成功的 API 調(diào)用,這確認(rèn)了 token 還是有效的。然后我們會檢查 X-OAuth-Scopes 應(yīng)答頭來確認(rèn)用戶還沒有撤消 user:email 區(qū)域。

創(chuàng)建一個名為 _advancedserver.rb 的文件,并且將下面的代碼粘貼到其中:

    require 'sinatra'
    require 'rest_client'
    require 'json'

    # !!! DO NOT EVER USE HARD-CODED VALUES IN A REAL APP !!!
    # Instead, set and test environment variables, like below
    # if ENV['GITHUB_CLIENT_ID'] && ENV['GITHUB_CLIENT_SECRET']
    #  CLIENT_ID        = ENV['GITHUB_CLIENT_ID']
    #  CLIENT_SECRET    = ENV['GITHUB_CLIENT_SECRET']
    # end

    CLIENT_ID = ENV['GH_BASIC_CLIENT_ID']
    CLIENT_SECRET = ENV['GH_BASIC_SECRET_ID']

    use Rack::Session::Pool, :cookie_only => false

    def authenticated?
      session[:access_token]
    end

    def authenticate!
      erb :index, :locals => {:client_id => CLIENT_ID}
    end

    get '/' do
      if !authenticated?
        authenticate!
      else
        access_token = session[:access_token]
        scopes = []

        begin
          auth_result = RestClient.get('https://api.github.com/user',
                                       {:params => {:access_token => access_token},
                                        :accept => :json})
        rescue => e
          # request didn't succeed because the token was revoked so we
          # invalidate the token stored in the session and render the
          # index page so that the user can start the OAuth flow again

          session[:access_token] = nil
          return authenticate!
        end

        # the request succeeded, so we check the list of current scopes
        if auth_result.headers.include? :x_oauth_scopes
          scopes = auth_result.headers[:x_oauth_scopes].split(', ')
        end

        auth_result = JSON.parse(auth_result)

        if scopes.include? 'user:email'
          auth_result['private_emails'] =
            JSON.parse(RestClient.get('https://api.github.com/user/emails',
                           {:params => {:access_token => access_token},
                            :accept => :json}))
        end

        erb :advanced, :locals => auth_result
      end
    end

    get '/callback' do
      session_code = request.env['rack.request.query_hash']['code']

      result = RestClient.post('https://github.com/login/oauth/access_token',
                              {:client_id => CLIENT_ID,
                               :client_secret => CLIENT_SECRET,
                               :code => session_code},
                               :accept => :json)

      session[:access_token] = JSON.parse(result)['access_token']

      redirect '/'
    end

大部分的代碼看起來都很熟悉。比如說,我們?nèi)匀皇褂?RestClient.get 來調(diào)用 GitHub API ,并且我們?nèi)匀粚⑽覀兊慕Y(jié)果傳給一個 ERB 模板來渲染。(這回,文件名是 advanced.erb

而且,我們現(xiàn)在使用 authenticated? 方法來檢查用戶是否已經(jīng)認(rèn)證過了。如果沒有,authenticate! 方法將被調(diào)用,這個方法將執(zhí)行 OAuth 流程并且使用被授 予的標(biāo)識和區(qū)域來更新回話。

接下來,在 views 中創(chuàng)建一個文件 advanced.erb,并將以下內(nèi)容粘貼進(jìn)去:

    <html>
      <head>
      </head>
      <body>
        <p>Well, well, well, <%= login %>!</p>
        <p>
          <% if !email.empty? %> It looks like your public email address is <%= email %>.
          <% else %> It looks like you don't have a public email. That's cool.
          <% end %>
        </p>
        <p>
          <% if defined? private_emails %>
          With your permission, we were also able to dig up your private email addresses:
          <%= private_emails.map{ |private_email_address| private_email_address["email"] }.join(', ') %>
          <% else %>
          Also, you're a bit secretive about your private email addresses.
          <% end %>
        </p>
      </body>
    </html>

從命令行調(diào)用 ruby advanced_server.rb ,將在 4567 端口啟動你的服務(wù)端,和 我們使用簡單的 Sinatra app 時同樣的端口。當(dāng)你瀏覽 http://localhost:4567時,app 調(diào)用 authenticate! 將你重定向到 /callback。然后 /callback 將我們又送回了 / ,由于現(xiàn)在我們已經(jīng)認(rèn)證了,頁面將渲染 advanced.erb

我們能夠通過在 GitHub 將我們的回調(diào) URL 指定為 / 來完全簡化這個往返的過程。但是,因為 server.rbadvanced.rb 都依賴于同一個回調(diào) URL,我們必須多繞點彎來讓它正確工作。

而且,如果我們從來沒有授權(quán)這個應(yīng)用去獲取我們的 GitHub 數(shù)據(jù),我們將從更早的彈出窗口看到相同的確認(rèn)對話框和警告。

如果你有興趣,你可以查看 yet another Sinatra-GitHub auth example 作為另一個工程進(jìn)行實驗。