在這一節(jié),我們將重點講身份認證的基礎知識。明確地說,我們將使用Sinatra創(chuàng)建一個 ruby 服務,該服務將用幾種不同的方式來實現(xiàn)一個應用的 web 流程。
你能夠從平臺范例倉庫下載這個工程的完整源代碼。
首先,你需要 注冊你的應用。每一個已注冊的 OAuth 應用將被指定一個唯一的 Client ID 和 Client Secret。注意不要共享你的 Client Secret!包括將該字符串提交到你的 repo 中。
你能夠根據(jù)你的喜好任意填寫每一個信息,除了授權回調 URL。它無疑是配置你的應用最重要的部分。它是 Github 在成功認證用戶之后返回的回調 URL。
因為我們是運行一個普通的 Sinatra 服務,本地實例的地址被設置為 http://localhost:4567
。所以讓我們將回調 URL 填寫為 http://localhost:4567/callback
。
現(xiàn)在,讓我們開始編寫我們簡單的服務。創(chuàng)建名為 server.rb 的文件并且將以下內容粘貼到文件中:
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 密匙來自你的 應用配置頁。你絕不應該將這些值存儲于 Github 或其他公共區(qū)域。我們建議將它們保存為 環(huán)境變量 ,這也是我們這里所做的。
接下來,在 views/index.erb 中,粘貼以下內容:
<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ù)來定義應用程序所要求的權限區(qū)域(scopes)。對于我們的應用,我們請求 user:email
權限區(qū)域來讀取私有 email 地址。
在你的瀏覽器中打開 http://localhost:4567
。點擊該鏈接后,你將跳轉至 GitHub,并且顯示類似以下對話框:
http://wiki.jikexueyuan.com/project/github-developer-guides/images/oauth_prompt.png" alt="" />
如果你信任你自己,點擊 Authorize App。哇哦, Sinatra 跳出來一個404
錯誤。這是怎么回事?
好吧,記得我們指定了一個回調 URL 為 callback
嗎?我們并沒有為它提供路由,所以 GitHub 在驗證 app 之后,不知道把用戶往哪里丟?,F(xiàn)在我們來解決這個問題!
在 server.rb 中,加入一個 route 來指明 callback 應該做什么:
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 授權認證之后, GitHub 提供了一個臨時的 code
值。你將需要將這個值 POST
回 GitHub 以交換一個 access_token
。我們使用rest-client來簡化我們的 GET 和 POST HTTP 請求。注意,你可能永遠不會通過 REST 來訪問這些 API 。對于一個更加正式的應用,你很可能使用一個你選擇的語言所寫的庫。
在此之后,用戶將能夠編輯你請求的權限區(qū)域,你的應用也可能被授予少于你默認請求的數(shù)量的權限區(qū)域。所以,在你使用該 token 進行任何請求前,你應該確定用戶授予了該 token 哪些權限區(qū)域。
被授予的權限區(qū)域被作為交換 token 時返回值的一部分被返回。
# check if we were granted user:email scope
scopes = JSON.parse(result)['scope'].split(',')
has_user_email_scope = scopes.include? 'user:email'
在我們的應用中,我們使用 scopes.include?
來檢查我們是否被授予了 user:email
區(qū)域的權限,我們需要使用該權限來獲取授權用戶的私人 email 地址。如果應用程序要求更多其他區(qū)域的權限,我們也可以以同樣的方式檢查。
還有,因為權限區(qū)域之間有著繼承的關系,你必須檢查你被授予了所請求的最低級別的權限。比如說,如果應用請求了 user
區(qū)域權限,但是它可能只被授予了 user:email
區(qū)域權限。在這種情況下,應用將不會被授予它所請求的權限,但是已經被授予的區(qū)域權限仍然是有效的。
僅在進行請求之前檢查區(qū)域授權情況是不夠的,因為用戶可能在你檢查授權情況和進行實際請求之間改變了區(qū)域權限。如果這種情況發(fā)生了,原本你預計成功的請求,可能會失敗并返回一個404
或401
狀態(tài),或者返回一個不同的信息子集。
為了讓你能夠更優(yōu)雅地處理這些情況,所有有效 token 發(fā)起的 API 請求的返回值都包含一個 X-OAuth-Scopes
頭部。這個頭部包含了該 token 用來發(fā)起請求的區(qū)域列表。除此之外,授權 API 還提供了一個終端來檢查一個 token 的有效性。使用這個信息來檢測 token 授權區(qū)域的改變,并且告知你的用戶可用應用功能的改變。
最后,使用這個 access token,你將能夠作為一個已登錄用戶發(fā)起已認證的請求。
# 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
我們能夠使用我們的結果做任何我們想要的事。在這里,我們僅僅是將它們直接輸出到 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>
如果我們要求用戶每次進入網頁的時候都需要登錄 app ,那是非常糟糕的。例如,嘗試直接打 http://localhost:4567/basic
。你將看到一個報錯。
假如我們能夠跳過整個“點擊這里”的過程,而是僅僅記住它,只要用戶登錄了 GitHub,它們就能夠使用這個應用,那會怎樣?請保持淡定,因為這就是接下來我們要做的。
我們上面縮寫的小服務器是非常簡單的。為了能夠嵌入一些智能的認證機制,我們將轉而使用回話來保存 token。這將使得認證對用戶來說是透明的。
另外,因為我們要在會話中保持授權區(qū)域,我們需要處理用戶在我們檢查之后更新了區(qū)域,或者撤消了標識的情況。為了做到這一點,我們將使用一個 rescue
區(qū)塊并檢查第一個成功的 API 調用,這確認了 token 還是有效的。然后我們會檢查 X-OAuth-Scopes
應答頭來確認用戶還沒有撤消 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
大部分的代碼看起來都很熟悉。比如說,我們仍然使用 RestClient.get
來調用 GitHub API ,并且我們仍然將我們的結果傳給一個 ERB 模板來渲染。(這回,文件名是 advanced.erb
)
而且,我們現(xiàn)在使用 authenticated?
方法來檢查用戶是否已經認證過了。如果沒有,authenticate!
方法將被調用,這個方法將執(zhí)行 OAuth 流程并且使用被授
予的標識和區(qū)域來更新回話。
接下來,在 views 中創(chuàng)建一個文件 advanced.erb,并將以下內容粘貼進去:
<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>
從命令行調用 ruby advanced_server.rb
,將在 4567 端口啟動你的服務端,和
我們使用簡單的 Sinatra app 時同樣的端口。當你瀏覽 http://localhost:4567
時,app 調用 authenticate!
將你重定向到 /callback
。然后 /callback
將我們又送回了 /
,由于現(xiàn)在我們已經認證了,頁面將渲染 advanced.erb 。
我們能夠通過在 GitHub 將我們的回調 URL 指定為 /
來完全簡化這個往返的過程。但是,因為 server.rb
和 advanced.rb
都依賴于同一個回調 URL,我們必須多繞點彎來讓它正確工作。
而且,如果我們從來沒有授權這個應用去獲取我們的 GitHub 數(shù)據(jù),我們將從更早的彈出窗口看到相同的確認對話框和警告。
如果你有興趣,你可以查看 yet another Sinatra-GitHub auth example 作為另一個工程進行實驗。