Skip to main content

使用 GitHub Apps 生成“使用 GitHub 登录”按钮

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

简介

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

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

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

如果你想要将应用操作归因于用户,应用应使用用户访问令牌。 有关详细信息,请参阅“代表用户使用 GitHub 应用进行身份验证”。

可通过两种方式为 GitHub App 生成用户访问令牌:Web 应用程序流和设备流。 如果应用有权访问 Web 界面,则应使用 Web 应用程序流。 如果应用无权访问 Web 界面,则应改用设备流。 有关详细信息,请参阅“为 GitHub 应用生成用户访问令牌”和“使用 GitHub Apps 生成 CLI”。

先决条件

本教程假定你已注册 GitHub App。 若要详细了解如何注册 GitHub App,请参阅“注册 GitHub 应用”。

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

本教程假定你已基本了解 Ruby 和 Ruby 模板系统 ERB。 有关详细信息,请参阅 RubyERB

安装依赖项

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

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

若要学习本教程,必须在 Ruby 项目中安装 Sinatra 和 dotenv gem。 例如,可以使用捆绑程序执行此操作:

  1. 如果尚未安装捆绑程序,请在终端中运行以下命令:

    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 Apps”。

  5. 在要使用的 GitHub App 旁边,单击“编辑”。

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

  7. 在应用的设置页上,单击“生成新的客户端密码”。 你将在后续步骤中将该客户端密码添加到 .env 文件中。

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

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

  10. .env 添加到 .gitignore 文件。 这可以防止意外提交客户端密码。 有关 .gitignore 文件的详细信息,请参阅“忽略文件”。

  11. 将以下内容添加到 .env 文件。 将 YOUR_CLIENT_ID 替换为应用的客户端 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="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>'
      erb link
    end
    
  5. 将以下代码添加到 app.rb,以处理对应用的回叫 URL 的请求,并从请求中获取 code 参数。 将 CALLBACK_URL 替换为应用的回叫 URL,减去域。 例如,如果回叫 URL 为 http://localhost: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="http(s)://HOSTNAME/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. 在浏览器中,导航到 http://localhost:4567。 应会看到一个链接,其中包含文本“使用 GitHub 登录”。

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

      如果尚未授权应用,则单击该链接后应会转到 http(s)://HOSTNAME/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("http(s)://HOSTNAME/login/oauth/access_token"),
        URI.encode_www_form(params),
        {"Accept" => "application/json"}
      )
    
      parse_response(result)
    end
    
    get "/" do
      link = '<a href="http(s)://HOSTNAME/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. 在浏览器中,导航到 http://localhost: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("http(s)://HOSTNAME/api/v3/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 为 http://localhost: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("http(s)://HOSTNAME/login/oauth/access_token"),
    URI.encode_www_form(params),
    {"Accept" => "application/json"}
  )

  parse_response(result)
end

def user_info(token)
  uri = URI("http(s)://HOSTNAME/api/v3/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="http(s)://HOSTNAME/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 http://localhost:4567

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

  2. 在浏览器中,导航到 http://localhost:4567。 应会看到一个链接,其中包含文本“使用 GitHub 登录”。

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

    如果尚未授权应用,则单击该链接后应会转到 http(s)://HOSTNAME/login/oauth/authorize?client_id=CLIENT_ID,其中 CLIENT_ID 是应用的客户端 ID。 这是一个 GitHub 页面,提示用户授权应用。 如果单击该按钮来授权应用,将会转到应用的回叫 URL。

    如果你之前已授权应用且授权尚未撤销,则会跳过授权提示并直接转到回叫 URL。 如果想要看到授权提示,可以撤销以前的授权。 有关详细信息,请参阅“查看和撤销 GitHub 应用的授权”。

  4. 单击“使用 GitHub 登录”链接,然后在系统提示时授权应用,即可访问回叫 URL 页面,该页面应显示如下文本:“已成功授权! 欢迎,Mona Lisa (octocat)”。

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

后续步骤

安全地存储客户端密码

切勿公开应用的客户端密码。 本教程将客户端密码存储在 git 忽略的 .env 文件中,并使用 ENV.fetch 访问值。 部署应用时,应选择一种安全的方式来存储客户端密码并更新代码以获取相应值。

例如,可以将机密存储在部署应用程序的服务器上的环境变量中。 还可以使用 Azure 密钥保管库等机密管理服务。

更新部署的回叫 URL

本教程使用以 http://localhost:4567 开头的回叫 URL。 但是,仅在启动 Sinatra 服务器时,http://localhost:4567 才对计算机本地可用。 在部署应用之前,应更新回叫 URL 以使用在生产环境中所使用的回叫 URL。 有关更新应用的回叫 URL 的详细信息,请参阅“修改 GitHub 应用注册”和“关于用户授权回调 URL”。

处理多个回叫 URL

本教程使用单个回叫 URL,但应用最多可以有 10 个回叫 URL。 如果要使用多个回叫 URL:

  • 将其他回叫 URL 添加到应用。 有关添加回叫 URL 的详细信息,请参阅“修改 GitHub 应用注册”。
  • 链接到 http(s)://HOSTNAME/login/oauth/authorize 时,使用 redirect_uri 查询参数将用户重定向到所需的回叫 URL。 有关详细信息,请参阅“为 GitHub 应用生成用户访问令牌”。
  • 在应用代码中,处理每个回叫 URL,类似于以 get "CALLBACK_URL" do 开头的代码块。

指定其他参数

链接到 http(s)://HOSTNAME/login/oauth/authorize 时,可以传递其他查询参数。 有关详细信息,请参阅“为 GitHub 应用生成用户访问令牌”。

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

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

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

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

安全地存储令牌

本教程可生成用户访问令牌。 除非选择不让用户访问令牌过期,否则用户访问令牌将在 8 小时后过期。 你还将收到可以重新生成用户访问令牌的刷新令牌。 有关详细信息,请参阅“刷新用户访问令牌”。

如果计划进一步与 GitHub 的 API 进行交互,则应存储令牌以供将来使用。 如果选择存储用户访问令牌或刷新令牌,则必须安全地存储它。 切勿公开令牌。

有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。

遵循最佳做法

你的目标应该是遵循 GitHub App 的最佳做法。 有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。