跳至主要内容

使用 GitHub 应用程序构建“使用 GitHub 登录”按钮

按照本教程编写 Ruby 代码,以通过 Web 应用程序流程为 GitHub 应用程序生成用户访问令牌。

简介

本教程演示如何为网站构建“使用 GitHub 登录”按钮。该网站将使用 GitHub 应用程序通过 Web 应用程序流程生成用户访问令牌。然后,该网站使用用户访问令牌代表经过身份验证的用户进行 API 请求。

本教程使用 Ruby,但你可以对任何用于 Web 开发的编程语言使用 Web 应用程序流程。

关于 Web 应用程序流程和用户访问令牌

如果你希望将应用程序的操作归因于用户,则你的应用程序应使用用户访问令牌。有关更多信息,请参阅“代表用户使用 GitHub 应用程序进行身份验证”。

有两种方法可以为 GitHub 应用程序生成用户访问令牌:Web 应用程序流程和设备流程。如果你的应用程序可以访问 Web 界面,则应使用 Web 应用程序流程。如果你的应用程序无法访问 Web 界面,则应改用设备流程。有关更多信息,请参阅“为 GitHub 应用程序生成用户访问令牌”和“使用 GitHub 应用程序构建 CLI”。

先决条件

本教程假设您已注册了 GitHub App。有关注册 GitHub App 的详细信息,请参阅“注册 GitHub App”。

在按照本教程操作之前,您必须为您的应用设置回调 URL。本教程使用本地 Sinatra 服务器,其默认 URL 为 https://127.0.0.1:4567。例如,要使用本地 Sinatra 应用程序的默认 URL,您的回调 URL 可以是 https://127.0.0.1:4567/github/callback。准备好部署应用后,您可以更改回调 URL 以使用您的实时服务器地址。有关更新应用的回调 URL 的详细信息,请参阅“修改 GitHub App 注册”和“关于用户授权回调 URL”。

本教程假设您对 Ruby 和 Ruby 模板系统 ERB 有基本的了解。有关详细信息,请参阅 RubyERB

安装依赖项

本教程使用 Ruby gem Sinatra 用 Ruby 创建 Web 应用程序。有关详细信息,请参阅 Sinatra 自述文件

本教程使用 Ruby gem dotenv 访问存储在 .env 文件中的值。有关详细信息,请参阅 dotenv 自述文件

要按照本教程操作,您必须在 Ruby 项目中安装 Sinatra 和 dotenv gem。例如,您可以使用 Bundler 这样做

  1. 如果您尚未安装 Bundler,请在终端中运行以下命令

    gem install bundler
    
  2. 如果您尚未为您的应用创建 Gemfile,请在终端中运行以下命令

    bundle init
    
  3. 如果您尚未为您的应用创建 Gemfile.lock,请在终端中运行以下命令

    bundle install
    
  4. 通过在终端中运行以下命令来安装 gem

    bundle add sinatra
    
    bundle add dotenv
    

存储客户端 ID 和客户端密钥

本教程将向您展示如何将客户端 ID 和客户端密钥存储在环境变量中,并使用 ENV.fetch 访问它们。部署应用时,您需要更改存储客户端 ID 和客户端密钥的方式。有关详细信息,请参阅“安全地存储您的客户端密钥”。

  1. 在 GitHub 上的任何页面的右上角,单击您的个人资料照片。

  2. 导航到您的帐户设置。

    • 对于个人帐户拥有的应用,请单击设置
    • 对于由组织拥有的应用
      1. 点击您的组织
      2. 在组织的右侧,点击设置
  3. 在左侧边栏中,点击 开发者设置

  4. 在左侧边栏中,点击GitHub 应用

  5. 在您想要操作的 GitHub 应用旁边,点击编辑

  6. 在应用的设置页面中,找到您应用的客户端 ID。您将在后续步骤中将其添加到 .env 文件中。请注意,客户端 ID 不同于应用 ID。

  7. 在应用的设置页面中,点击生成新的客户端密钥。您将在后续步骤中将客户端密钥添加到 .env 文件中。

  8. 在与 Gemfile 相同的级别创建一个名为 .env 的文件。

  9. 如果您的项目还没有 .gitignore 文件,请在与 Gemfile 相同的级别创建一个 .gitignore 文件。

  10. .env 添加到 .gitignore 文件中。这将防止您意外提交您的客户端密钥。有关 .gitignore 文件的更多信息,请参阅“忽略文件”。

  11. 将以下内容添加到您的 .env 文件中。用您应用的客户端 ID 替换 YOUR_CLIENT_ID。用您应用的客户端密钥替换 YOUR_CLIENT_SECRET

    CLIENT_ID="YOUR_CLIENT_ID"
    CLIENT_SECRET="YOUR_CLIENT_SECRET"
    

添加代码以生成用户访问令牌

要获取用户访问令牌,您首先需要提示用户授权您的应用。当用户授权您的应用时,他们会被重定向到您应用的回调 URL。对回调 URL 的请求包括一个 code 查询参数。当您的应用收到为该回调 URL 提供服务的请求时,您可以用 code 参数交换用户访问令牌。

这些步骤将指导您编写代码以生成用户访问令牌。要跳到最终代码,请参阅“完整代码示例”。

  1. 在与 .env 文件相同的目录中,创建一个 Ruby 文件来保存将生成用户访问令牌的代码。本教程将把该文件命名为 app.rb

  2. app.rb 的顶部,添加这些依赖项

    Ruby
    require "sinatra"
    require "dotenv/load"
    require "net/http"
    require "json"
    

    sinatradotenv/load 依赖项使用您之前安装的 gem。net/httpjson 是 Ruby 标准库的一部分。

  3. 将以下代码添加到 app.rb,以从 .env 文件中获取应用的客户端 ID 和客户端密钥。

    Ruby
    CLIENT_ID = ENV.fetch("CLIENT_ID")
    CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
    
  4. 将以下代码添加到 app.rb,以显示一个链接,该链接将提示用户验证您的应用。

    Ruby
    get "/" do
      link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
      erb link
    end
    
  5. 将以下代码添加到 app.rb,以处理对应用的回调 URL 的请求并从请求中获取 code 参数。将 CALLBACK_URL 替换为应用的回调 URL,减去域名。例如,如果您的回调 URL 是 https://127.0.0.1:4567/github/callback,请将 CALLBACK_URL 替换为 /github/callback

    Ruby
    get "CALLBACK_URL" do
      code = params["code"]
      render = "Successfully authorized! Got code #{code}."
      erb render
    end
    

    目前,该代码仅呈现一条消息以及 code 参数。以下步骤将扩展此代码块。

  6. (可选)检查您的进度

    app.rb 现在看起来像这样,其中 CALLBACK_URL 是应用的回调 URL,减去域名

    Ruby
    require "sinatra"
    require "dotenv/load"
    require "net/http"
    require "json"
    
    CLIENT_ID = ENV.fetch("CLIENT_ID")
    CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
    
    get "/" do
      link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
      erb link
    end
    
    get "CALLBACK_URL" do
      code = params["code"]
      render = "Successfully authorized! Got code #{code}."
      erb render
    end
    
    1. 在您的终端中,从存储 app.rb 的目录中,运行 ruby app.rb。本地 Sinatra 服务器应启动。

    2. 在您的浏览器中,导航到 https://127.0.0.1:4567。您应该会看到一个文本为“使用 GitHub 登录”的链接。

    3. 点击“使用 GitHub 登录”链接。

      如果您尚未授权该应用,点击该链接应会将您带到 https://github.com/login/oauth/authorize?client_id=CLIENT_ID,其中 CLIENT_ID 是您应用的客户端 ID。这是一个 GitHub 页面,提示用户授权您的应用。如果您点击按钮以授权您的应用,您将转到应用的回调 URL。

      如果您之前已授权您的应用并且授权尚未被撤销,您将跳过授权提示并直接转到回调 URL。如果您想看到授权提示,您可以撤销您之前的授权。有关更多信息,请参阅“查看和撤销 GitHub 应用的授权”。

    4. 通过单击“使用 GitHub 登录”链接,然后在提示时授权该应用,可以到达回调 URL 页面,该页面应显示类似于“成功授权!获取代码 agc622abb6135be5d1f2”的文本。

    5. 在 Sinatra 正在运行的终端中,通过输入 Ctrl+C 停止服务器。

  7. app.rb 的内容替换为以下代码,其中 CALLBACK_URL 是您的应用的回调 URL,不包括域名。

    此代码添加了用于交换 code 参数以获取用户访问令牌的逻辑

    • parse_response 函数解析来自 GitHub API 的响应。
    • exchange_code 函数交换 code 参数以获取用户访问令牌。
    • 回调 URL 请求的处理程序现在调用 exchange_code 以交换代码参数以获取用户访问令牌。
    • 回调页面现在显示文本以指示已生成令牌。如果令牌生成不成功,该页面将指示该失败。
    Ruby
    require "sinatra"
    require "dotenv/load"
    require "net/http"
    require "json"
    
    CLIENT_ID = ENV.fetch("CLIENT_ID")
    CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
    
    def parse_response(response)
      case response
      when Net::HTTPOK
        JSON.parse(response.body)
      else
        puts response
        puts response.body
        {}
      end
    end
    
    def exchange_code(code)
      params = {
        "client_id" => CLIENT_ID,
        "client_secret" => CLIENT_SECRET,
        "code" => code
      }
      result = Net::HTTP.post(
        URI("https://github.com/login/oauth/access_token"),
        URI.encode_www_form(params),
        {"Accept" => "application/json"}
      )
    
      parse_response(result)
    end
    
    get "/" do
      link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
      erb link
    end
    
    get "CALLBACK_URL" do
      code = params["code"]
    
      token_data = exchange_code(code)
    
      if token_data.key?("access_token")
        token = token_data["access_token"]
    
        render = "Successfully authorized! Got code #{code} and exchanged it for a user access token ending in #{token[-9..-1]}."
        erb render
      else
        render = "Authorized, but unable to exchange code #{code} for token."
        erb render
      end
    end
    
  8. (可选)检查您的进度

    1. 在您的终端中,从存储 app.rb 的目录中,运行 ruby app.rb。本地 Sinatra 服务器应启动。
    2. 在您的浏览器中,导航到 https://127.0.0.1:4567。您应该会看到一个文本为“使用 GitHub 登录”的链接。
    3. 点击“使用 GitHub 登录”链接。
    4. 在提示时,授权您的应用。
    5. 通过单击“使用 GitHub 登录”链接,然后在提示时授权该应用,可以到达回调 URL 页面,该页面应显示类似于“成功授权!获取代码 4acd44861aeda86dacce 并将其交换为以 2zU5kQziE 结尾的用户访问令牌”的文本。
    6. 在 Sinatra 正在运行的终端中,通过输入 Ctrl+C 停止服务器。
  9. 现在您已拥有用户访问令牌,您可以使用该令牌代表用户发出 API 请求。例如

    将此函数添加到 app.rb 以获取有关使用 /user REST API 端点获取用户信息的信息

    Ruby
    def user_info(token)
      uri = URI("https://api.github.com/user")
    
      result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
        body = {"access_token" => token}.to_json
    
        auth = "Bearer #{token}"
        headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth}
    
        http.send_request("GET", uri.path, body, headers)
      end
    
      parse_response(result)
    end
    

    更新回调处理程序以调用 user_info 函数并显示用户的姓名和 GitHub 登录名。请记住将 CALLBACK_URL 替换为您的应用的回调 URL,不包括域名。

    Ruby
    get "CALLBACK_URL" do
      code = params["code"]
    
      token_data = exchange_code(code)
    
      if token_data.key?("access_token")
        token = token_data["access_token"]
    
        user_info = user_info(token)
        handle = user_info["login"]
        name = user_info["name"]
    
        render = "Successfully authorized! Welcome, #{name} (#{handle})."
        erb render
      else
        render = "Authorized, but unable to exchange code #{code} for token."
        erb render
      end
    end
    
  10. 根据完整代码示例中的完整代码检查您的代码。您可以按照完整代码示例下方的“测试”部分中概述的步骤来测试您的代码。

完整代码示例

这是上一部分中概述的完整代码示例。

CALLBACK_URL 替换为应用的回调 URL,不包括域名。例如,如果你的回调 URL 是 https://127.0.0.1:4567/github/callback,则将 CALLBACK_URL 替换为 /github/callback

Ruby
require "sinatra"
require "dotenv/load"
require "net/http"
require "json"

CLIENT_ID = ENV.fetch("CLIENT_ID")
CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")

def parse_response(response)
  case response
  when Net::HTTPOK
    JSON.parse(response.body)
  else
    puts response
    puts response.body
    {}
  end
end

def exchange_code(code)
  params = {
    "client_id" => CLIENT_ID,
    "client_secret" => CLIENT_SECRET,
    "code" => code
  }
  result = Net::HTTP.post(
    URI("https://github.com/login/oauth/access_token"),
    URI.encode_www_form(params),
    {"Accept" => "application/json"}
  )

  parse_response(result)
end

def user_info(token)
  uri = URI("https://api.github.com/user")

  result = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
    body = {"access_token" => token}.to_json

    auth = "Bearer #{token}"
    headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth}

    http.send_request("GET", uri.path, body, headers)
  end

  parse_response(result)
end

get "/" do
  link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
  erb link
end

get "CALLBACK_URL" do
  code = params["code"]

  token_data = exchange_code(code)

  if token_data.key?("access_token")
    token = token_data["access_token"]

    user_info = user_info(token)
    handle = user_info["login"]
    name = user_info["name"]

    render = "Successfully authorized! Welcome, #{name} (#{handle})."
    erb render
  else
    render = "Authorized, but unable to exchange code #{code} for token."
    erb render
  end
end

测试

本教程假设你的应用代码存储在一个名为 app.rb 的文件中,并且你使用的是本地 Sinatra 应用程序的默认 URL https://127.0.0.1:4567

  1. 在您的终端中,从存储 app.rb 的目录中,运行 ruby app.rb。本地 Sinatra 服务器应启动。

  2. 在您的浏览器中,导航到 https://127.0.0.1:4567。您应该会看到一个文本为“使用 GitHub 登录”的链接。

  3. 点击“使用 GitHub 登录”链接。

    如果您尚未授权该应用,点击该链接应会将您带到 https://github.com/login/oauth/authorize?client_id=CLIENT_ID,其中 CLIENT_ID 是您应用的客户端 ID。这是一个 GitHub 页面,提示用户授权您的应用。如果您点击按钮以授权您的应用,您将转到应用的回调 URL。

    如果您之前已授权您的应用并且授权尚未被撤销,您将跳过授权提示并直接转到回调 URL。如果您想看到授权提示,您可以撤销您之前的授权。有关更多信息,请参阅“查看和撤销 GitHub 应用的授权”。

  4. 回调 URL 页面,通过点击“使用 GitHub 登录”链接,然后在提示时授权该应用即可访问,应显示类似于“授权成功!欢迎,Mona Lisa (octocat)。”的文本。

  5. 在 Sinatra 正在运行的终端中,通过输入 Ctrl+C 停止服务器。

后续步骤

安全存储你的客户端密钥

你永远不应公开你的应用的客户端密钥。本教程将客户端密钥存储在被 gitignore 的 .env 文件中,并使用 ENV.fetch 访问该值。当你部署你的应用时,你应选择一种安全的方式来存储客户端密钥,并相应地更新你的代码以获取该值。

例如,你可以将密钥存储在应用部署到的服务器上的环境变量中。你还可以使用秘密管理服务,如 Azure Key Vault。

更新用于部署的回调 URL

本教程使用了一个以 https://127.0.0.1:4567 开头的回调 URL。但是,https://127.0.0.1:4567 仅在你启动 Sinatra 服务器时在本地计算机上可用。在部署你的应用之前,你应更新回调 URL 以使用你在生产中使用的回调 URL。有关更新应用的回调 URL 的更多信息,请参阅“修改 GitHub 应用注册”和“关于用户授权回调 URL”。

处理多个回调 URL

本教程使用了一个回调 URL,但你的应用最多可以有 10 个回调 URL。如果你想使用多个回调 URL

  • 将其他回调 URL 添加到你的应用。有关添加回调 URL 的更多信息,请参阅“修改 GitHub 应用注册”。
  • 当你链接到 https://github.com/login/oauth/authorize 时,使用 redirect_uri 查询参数将用户重定向到所需的回调 URL。有关更多信息,请参阅“为 GitHub 应用生成用户访问令牌”。
  • 在你的应用代码中,处理每个回调 URL,类似于从 get "CALLBACK_URL" do 开始的代码块。

指定其他参数

当你链接到 https://github.com/login/oauth/authorize 时,你可以传递其他查询参数。有关更多信息,请参阅“为 GitHub 应用生成用户访问令牌”。

与传统的 OAuth 令牌不同,用户访问令牌不使用范围,因此你无法通过 scope 参数指定范围。相反,它使用细粒度的权限。用户访问令牌仅具有用户和应用都拥有的权限。

调整代码以满足你的应用需求

本教程演示了如何显示有关经过身份验证的用户的信息,但你可以调整此代码来执行其他操作。请记住,如果你的应用需要对想要进行的 API 请求有其他权限,请更新你的应用权限。有关更多信息,请参阅“为 GitHub 应用选择权限”。

本教程将所有代码存储到一个文件中,但你可能希望将函数和组件移动到单独的文件中。

安全地存储令牌

本教程生成用户访问令牌。除非你选择不为用户访问令牌设置过期时间,否则用户访问令牌将在八小时后过期。你还会收到一个可以重新生成用户访问令牌的刷新令牌。有关更多信息,请参阅“刷新用户访问令牌”。

如果你计划进一步与 GitHub 的 API 交互,你应该存储令牌以备将来使用。如果你选择存储用户访问令牌或刷新令牌,你必须安全地存储它。你永远不应该公开令牌。

有关更多信息,请参阅“创建 GitHub 应用的最佳实践”。

遵循最佳实践

您应该以 GitHub 应用为目标遵循最佳实践。有关详细信息,请参阅“创建 GitHub 应用的最佳实践”。