跳到主要内容

使用 OAuth 应用认证 REST API

了解使用一些示例进行身份验证的不同方法。

在本节中,我们将重点介绍身份验证的基础知识。具体来说,我们将创建一个 Ruby 服务器(使用 Sinatra),它以几种不同的方式实现应用程序的 网络流程

你可以从 platform-samples 仓库 下载此项目的完整源代码。

注册你的应用

首先,你需要注册你的应用程序。每个注册的 OAuth 应用程序都会分配一个唯一的客户端 ID 和客户端密钥。客户端密钥用于获取已登录用户的访问令牌。你必须在你的原生应用程序中包含客户端密钥,但是 web 应用程序不应泄露此值。

你可以随意填写其他所有信息,除了**授权回调 URL**。这是安全设置应用程序最重要的部分。它是 GitHub 在成功身份验证后将用户返回到的回调 URL。拥有该 URL 可以确保用户登录到你的应用程序,而不是将令牌泄露给攻击者。

由于我们运行的是常规 Sinatra 服务器,本地实例的位置设置为http://127.0.0.1:4567。让我们将回调 URL 填写为http://127.0.0.1:4567/callback

接受用户授权

已弃用通知:GitHub 不再通过查询参数使用 API。API 的身份验证应使用HTTP 基本身份验证。有关更多信息,包括计划中的服务中断,请参阅博文

现在,让我们开始填写我们的简单服务器。创建一个名为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

你的客户端 ID 和客户端密钥来自你的应用程序的配置页面。我们建议将这些值存储为环境变量,以便于替换和使用——这正是我们在这里所做的。

接下来,在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!
    </p>
    <p>
      If that link doesn't work, remember to provide your own <a href="/apps/building-oauth-apps/authorizing-oauth-apps/">Client ID</a>!
    </p>
  </body>
</html>

(如果你不熟悉 Sinatra 的工作原理,我们建议你阅读 Sinatra 指南。)

此外,请注意,该 URL 使用scope查询参数来定义应用程序请求的范围。对于我们的应用程序,我们请求user:email范围以读取私人电子邮件地址。

将浏览器导航到http://127.0.0.1:4567。单击链接后,你应该会被带到 GitHub,并显示“授权应用程序”对话框。

如果你信任自己,请单击**授权应用程序**。糟糕!Sinatra 吐出了一个404错误。怎么回事?!

嗯,还记得我们指定回调 URL 为callback吗?我们没有为此提供路由,因此 GitHub 不知道在用户授权应用程序后将用户放在哪里。让我们现在修复它!

提供回调

server.rb中,添加一个路由来指定回调应该做什么

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

成功进行应用程序身份验证后,GitHub 会提供一个临时的code值。你需要使用你的客户端密钥将此代码POST回 GitHub 以换取access_token。为了简化我们的 GET 和 POST HTTP 请求,我们使用的是rest-client。请注意,你可能永远不会通过 REST 访问 API。对于更重要的应用程序,你可能应该使用你选择的语言编写的库

检查授予的范围

用户可以通过直接更改 URL 来编辑你请求的范围。这可能会授予你的应用程序比你最初请求的更少的访问权限。在使用令牌发出任何请求之前,请检查用户为令牌授予的范围。有关请求的范围和授予的范围的更多信息,请参阅“OAuth 应用程序的范围”。

授予的范围作为交换令牌的响应的一部分返回。

get '/callback' do
  # ...
  # Get the access_token using the code sample above
  # ...

  # 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'
end

在我们的应用程序中,我们使用scopes.include?来检查我们是否被授予了获取已认证用户的私人电子邮件地址所需的user:email范围。如果应用程序请求了其他范围,我们也会检查这些范围。

此外,由于范围之间存在层次关系,因此你应该检查你是否被授予了所需范围的任何更高级别。例如,如果应用程序请求了user范围,则不会明确授予它user:email范围。在这种情况下,它将收到具有user范围的令牌,该令牌可用于请求用户的电子邮件地址,即使它在令牌上没有明确包含user:email。检查useruser:email都可以确保你检查这两种情况。

仅在发出请求之前检查范围是不够的,因为用户可能会在你检查和实际请求之间更改范围。如果发生这种情况,你预期会成功的 API 调用可能会失败,并返回404401状态,或者返回不同的信息子集。

为了帮助你优雅地处理这些情况,使用有效的 OAuth 应用程序令牌发出的请求的所有 API 响应还包含一个X-OAuth-Scopes标头。此标头包含用于发出请求的令牌的范围列表。除此之外,REST API 还提供了一个端点来检查令牌的有效性。使用此信息检测令牌范围的变化,并告知用户可用应用程序功能的变化。

发出经过身份验证的请求

最后,使用此访问令牌,你将能够以登录用户的身份发出经过身份验证的请求

# 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}}))
end

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>

实现“持久性”身份验证

如果我们要求用户每次需要访问网页时都登录应用程序,那将是一个非常糟糕的模型。例如,尝试直接导航到http://127.0.0.1:4567/basic。你将收到错误。

如果我们可以绕过整个“点击此处”过程,并且只要用户登录到 GitHub,他们就应该能够访问此应用程序,该怎么办?准备好你的帽子,因为这正是我们要做的

我们上面的小型服务器相当简单。为了插入一些智能身份验证,我们将切换到使用会话来存储令牌。这将使身份验证对用户透明。

此外,由于我们在会话中持久化范围,因此我们需要处理用户在我们检查范围后更新范围或撤销令牌的情况。为此,我们将使用rescue块并检查第一个 API 调用是否成功,这验证令牌是否仍然有效。之后,我们将检查X-OAuth-Scopes响应标头以验证用户是否已撤销user:email范围。

创建一个名为advanced_server.rb的文件,并将这些行粘贴到其中

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

# Don't use hard-coded values in your 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)。

此外,我们现在有了authenticated?方法,该方法检查用户是否已通过身份验证。如果没有,则调用authenticate!方法,该方法执行 OAuth 流程并使用授予的令牌和范围更新会话。

接下来,在views中创建一个名为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 应用程序时使用的端口相同。当你导航到http://127.0.0.1:4567时,应用程序调用authenticate!,这会将你重定向到/callback/callback然后将我们送回/,并且由于我们已通过身份验证,因此呈现advanced.erb

我们可以通过简单地将 GitHub 中的回调 URL 更改为/来完全简化此往返路由。但是,由于server.rbadvanced.rb都依赖于相同的回调 URL,因此我们必须做一些奇怪的事情才能使其工作。

此外,如果我们从未授权此应用程序访问我们的 GitHub 数据,我们将看到前面弹出的相同的确认对话框并警告我们。