简介
本教程演示如何为网站生成“使用 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。 有关详细信息,请参阅 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 上任意页的右上角,单击你的个人资料照片。
-
导航到你的帐户设置。
- 对于由个人帐户拥有的应用,请单击“设置”****。
- 对于组织拥有的应用:
- 单击“你的组织”。
- 在组织的右侧,单击设置。
-
在左侧边栏中,单击“ 开发人员设置”。
-
在左侧边栏中,单击“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="http(s)://HOSTNAME/login/oauth/authorize?client_id=<%= CLIENT_ID %>">Login with GitHub</a>' erb link end
get "/" do link = '<a href="http(s)://HOSTNAME/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="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
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
-
在终端中,从存储
app.rb
的目录中运行ruby app.rb
。 此时应会启动本地 Sinatra 服务器。 -
在浏览器中,导航到
http://localhost:4567
。 应会看到一个链接,其中包含文本“使用 GitHub 登录”。 -
单击“使用 GitHub 登录”链接。
如果尚未授权应用,则单击该链接后应会转到
http(s)://HOSTNAME/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("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
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
- 函数
-
(可选)检查进度:
- 在终端中,从存储
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("http(s)://HOSTNAME/api/v3/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("http(s)://HOSTNAME/api/v3/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("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| 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="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
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|
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="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
。
-
在终端中,从存储
app.rb
的目录中运行ruby app.rb
。 此时应会启动本地 Sinatra 服务器。 -
在浏览器中,导航到
http://localhost:4567
。 应会看到一个链接,其中包含文本“使用 GitHub 登录”。 -
单击“使用 GitHub 登录”链接。
如果尚未授权应用,则单击该链接后应会转到
http(s)://HOSTNAME/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 应用注册”。
- 链接到
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 应用的最佳做法”。