简介
本教程演示如何为网站生成“使用 GitHub 登录”按钮。 网站将使用 GitHub App 通过 Web 应用程序流生成用户访问令牌。 然后,网站使用用户访问令牌代表经过身份验证的用户发出 API 请求。
本教程使用 Ruby,但你也可以将 Web 应用程序流与用于 Web 开发的任何编程语言结合使用。
Note
本文包含使用 github.com
域的命令或示例。 可以在其他域(例如 octocorp.ghe.com
)中访问 GitHub。
关于 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。 有关详细信息,请参阅 Ruby 和 ERB。
安装依赖项
本教程使用 Ruby gem Sinatra 通过 Ruby 创建 Web 应用程序。 有关详细信息,请参阅 Sinatra 自述文件。
本教程使用 Ruby gem dotenv 访问存储在 .env
文件中的值。 有关详细信息,请参阅 dotenv 自述文件。
若要学习本教程,必须在 Ruby 项目中安装 Sinatra 和 dotenv gem。 例如,可以使用捆绑程序执行此操作:
-
如果尚未安装捆绑程序,请在终端中运行以下命令:
gem install bundler
-
如果应用还没有 Gemfile,请在终端中运行以下命令:
bundle init
-
如果应用还没有 Gemfile.lock,请在终端中运行以下命令:
bundle install
-
通过在终端中运行以下命令来安装 gem:
bundle add sinatra
bundle add dotenv
存储客户端 ID 和客户端密码
本教程介绍如何将客户端 ID 和客户端密码存储在环境变量中,并使用 ENV.fetch
进行访问。 部署应用时,需要更改客户端 ID 和客户端密码的存储方式。 有关详细信息,请参阅安全地存储客户端密码。
-
在 GitHub 上任意页的右上角,单击你的个人资料照片。
-
导航到你的帐户设置。
- 对于由个人帐户拥有的应用,请单击“设置”****。
- 对于组织拥有的应用:
- 单击“你的组织”。
- 在组织的右侧,单击设置。
- 对于由企业拥有的应用:
- 如果使用的是 Enterprise Managed Users,请单击你的企业,以直接转到企业帐户设置。
- 如果使用的是个人帐户,请单击你的企业,然后单击企业右侧的设置。
-
导航到 GitHub App 设置。
- 对于由个人帐户或组织拥有的应用:
- 在左侧边栏中,单击 开发人员设置,然后单击 GitHub Apps。
- 对于由企业拥有的应用:
- 在左侧边栏中,在“设置”下,单击 GitHub Apps。
- 对于由个人帐户或组织拥有的应用:
-
在要使用的 GitHub App 旁边,单击“编辑”。
-
在应用的“设置”页上,找到应用的客户端 ID。 你将在后续步骤中将其添加到
.env
文件中。 请注意,客户端 ID 不同于应用 ID。 -
在应用的设置页上,单击“生成新的客户端密码”。 你将在后续步骤中将该客户端密码添加到
.env
文件中。 -
在与
Gemfile
相同的级别创建名为.env
的文件。 -
如果项目还没有
.gitignore
文件,请在与Gemfile
相同的级别创建.gitignore
文件。 -
将
.env
添加到.gitignore
文件。 这可以防止意外提交客户端密码。 有关.gitignore
文件的详细信息,请参阅“忽略文件”。 -
将以下内容添加到
.env
文件。 将YOUR_CLIENT_ID
替换为应用的客户端 ID。 将YOUR_CLIENT_SECRET
替换为应用的客户端密码。CLIENT_ID="YOUR_CLIENT_ID" CLIENT_SECRET="YOUR_CLIENT_SECRET"
添加代码以生成用户访问令牌
若要获取用户访问令牌,首先需要提示用户授权应用。 当用户授权应用时,他们会重定向到应用的回叫 URL。 对回叫 URL 的请求包括 code
查询参数。 当应用收到提供该回叫 URL 的请求时,可以将 code
参数交换为用户访问令牌。
这些步骤将引导你编写代码以生成用户访问令牌。 若要跳到最终代码,请参阅完整代码示例。
-
在
.env
文件所在的同一目录中,创建一个 Ruby 文件来保存将生成用户访问令牌的代码。 本教程将该文件命名为app.rb
。 -
在
app.rb
的顶部,添加以下依赖项:Ruby require "sinatra" require "dotenv/load" require "net/http" require "json"
require "sinatra" require "dotenv/load" require "net/http" require "json"
sinatra
和dotenv/load
依赖项使用之前安装的 gem。net/http
和json
是 Ruby 标准库的一部分。 -
将以下代码添加到
app.rb
,以便从.env
文件中获取应用的客户端 ID 和客户端密码。Ruby CLIENT_ID = ENV.fetch("CLIENT_ID") CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
CLIENT_ID = ENV.fetch("CLIENT_ID") CLIENT_SECRET = ENV.fetch("CLIENT_SECRET")
-
将以下代码添加到
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
get "/" do link = '<a href="https://github.com/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end
-
将以下代码添加到
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
get "CALLBACK_URL" do code = params["code"] render = "Successfully authorized! Got code #{code}." erb render end
目前,代码仅显示一条消息以及
code
参数。 以下步骤将展开此代码块。 -
(可选)检查进度:
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
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
-
在终端中,从存储
app.rb
的目录中运行ruby app.rb
。 此时应会启动本地 Sinatra 服务器。 -
在浏览器中,导航到
http://localhost:4567
。 应会看到一个链接,其中包含文本“使用 GitHub 登录”。 -
单击“使用 GitHub 登录”链接。
如果尚未授权应用,则单击该链接后应会转到
https://github.com/login/oauth/authorize?client_id=CLIENT_ID
,其中CLIENT_ID
是应用的客户端 ID。 这是一个 GitHub 页面,提示用户授权应用。 如果单击该按钮来授权应用,将会转到应用的回叫 URL。如果你之前已授权应用且授权尚未撤销,则会跳过授权提示并直接转到回叫 URL。 如果想要看到授权提示,可以撤销以前的授权。 有关详细信息,请参阅“查看和撤销 GitHub 应用的授权”。
-
单击“使用 GitHub 登录”链接,然后在系统提示时授权应用,即可访问回叫 URL 页面,该页面应显示如下文本:“已成功授权! 已获取代码 agc622abb6135be5d1f2”。
-
在运行 Sinatra 的终端中,输入 Ctrl+C 来停止服务器。
-
-
将
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
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
- 函数
-
(可选)检查进度:
- 在终端中,从存储
app.rb
的目录中运行ruby app.rb
。 此时应会启动本地 Sinatra 服务器。 - 在浏览器中,导航到
http://localhost:4567
。 应会看到一个链接,其中包含文本“使用 GitHub 登录”。 - 单击“使用 GitHub 登录”链接。
- 如果系统提示执行此操作,请授权应用。
- 单击“使用 GitHub 登录”链接,然后在系统提示时授权应用,即可访问回叫 URL 页面,该页面应显示如下文本:“已成功授权! 已获取代码 4acd44861aeda86dacce,并将其交换为以 2zU5kQziE 结尾的用户访问令牌”。
- 在运行 Sinatra 的终端中,输入 Ctrl+C 来停止服务器。
- 在终端中,从存储
-
有了用户访问令牌后,可以使用该令牌代表用户发出 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| auth = "Bearer #{token}" headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth} http.send_request("GET", uri.path, nil, headers) end 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| auth = "Bearer #{token}" headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth} http.send_request("GET", uri.path, nil, 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
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
-
根据下一部分中的完整代码示例检查代码。 可以按照完整代码示例下方测试一节中概述的步骤测试代码。
完整代码示例
这是上一部分概述的完整代码示例。
将 CALLBACK_URL
替换为应用的回叫 URL,减去域。 例如,如果回叫 URL 为 http://localhost:4567/github/callback
,请将 CALLBACK_URL
替换为 /github/callback
。
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| auth = "Bearer #{token}" headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth} http.send_request("GET", uri.path, nil, 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
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|
auth = "Bearer #{token}"
headers = {"Accept" => "application/json", "Content-Type" => "application/json", "Authorization" => auth}
http.send_request("GET", uri.path, nil, 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 http://localhost:4567
。
-
在终端中,从存储
app.rb
的目录中运行ruby app.rb
。 此时应会启动本地 Sinatra 服务器。 -
在浏览器中,导航到
http://localhost:4567
。 应会看到一个链接,其中包含文本“使用 GitHub 登录”。 -
单击“使用 GitHub 登录”链接。
如果尚未授权应用,则单击该链接后应会转到
https://github.com/login/oauth/authorize?client_id=CLIENT_ID
,其中CLIENT_ID
是应用的客户端 ID。 这是一个 GitHub 页面,提示用户授权应用。 如果单击该按钮来授权应用,将会转到应用的回叫 URL。如果你之前已授权应用且授权尚未撤销,则会跳过授权提示并直接转到回叫 URL。 如果想要看到授权提示,可以撤销以前的授权。 有关详细信息,请参阅“查看和撤销 GitHub 应用的授权”。
-
单击“使用 GitHub 登录”链接,然后在系统提示时授权应用,即可访问回叫 URL 页面,该页面应显示如下文本:“已成功授权! 欢迎,Mona Lisa (octocat)”。
-
在运行 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 应用注册”。
- 链接到
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 Apps 选择权限”。
本教程将所有代码存储在单个文件中,但你可能希望将函数和组件移动到单独的文件中。
安全地存储令牌
本教程可生成用户访问令牌。 除非选择不让用户访问令牌过期,否则用户访问令牌将在 8 小时后过期。 你还将收到可以重新生成用户访问令牌的刷新令牌。 有关详细信息,请参阅“刷新用户访问令牌”。
如果计划进一步与 GitHub 的 API 进行交互,则应存储令牌以供将来使用。 如果选择存储用户访问令牌或刷新令牌,则必须安全地存储它。 切勿公开令牌。
有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。
遵循最佳做法
你的目标应该是遵循 GitHub App 的最佳做法。 有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。