本教程演示如何为网站构建“使用 GitHub 登录”按钮。该网站将使用 GitHub 应用程序通过 Web 应用程序流程生成用户访问令牌。然后,该网站使用用户访问令牌代表经过身份验证的用户进行 API 请求。
本教程使用 Ruby,但你可以对任何用于 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 有基本的了解。有关详细信息,请参阅 Ruby 和 ERB。
本教程使用 Ruby gem Sinatra 用 Ruby 创建 Web 应用程序。有关详细信息,请参阅 Sinatra 自述文件。
本教程使用 Ruby gem dotenv 访问存储在 .env
文件中的值。有关详细信息,请参阅 dotenv 自述文件。
要按照本教程操作,您必须在 Ruby 项目中安装 Sinatra 和 dotenv gem。例如,您可以使用 Bundler 这样做
-
如果您尚未安装 Bundler,请在终端中运行以下命令
gem install bundler
-
如果您尚未为您的应用创建 Gemfile,请在终端中运行以下命令
bundle init
-
如果您尚未为您的应用创建 Gemfile.lock,请在终端中运行以下命令
bundle install
-
通过在终端中运行以下命令来安装 gem
bundle add sinatra
bundle add dotenv
本教程将向您展示如何将客户端 ID 和客户端密钥存储在环境变量中,并使用 ENV.fetch
访问它们。部署应用时,您需要更改存储客户端 ID 和客户端密钥的方式。有关详细信息,请参阅“安全地存储您的客户端密钥”。
-
在 GitHub 上的任何页面的右上角,单击您的个人资料照片。
-
导航到您的帐户设置。
- 对于个人帐户拥有的应用,请单击设置。
- 对于由组织拥有的应用
- 点击您的组织。
- 在组织的右侧,点击设置。
-
在左侧边栏中,点击 开发者设置。
-
在左侧边栏中,点击GitHub 应用。
-
在您想要操作的 GitHub 应用旁边,点击编辑。
-
在应用的设置页面中,找到您应用的客户端 ID。您将在后续步骤中将其添加到 .env
文件中。请注意,客户端 ID 不同于应用 ID。
-
在应用的设置页面中,点击生成新的客户端密钥。您将在后续步骤中将客户端密钥添加到 .env
文件中。
-
在与 Gemfile
相同的级别创建一个名为 .env
的文件。
-
如果您的项目还没有 .gitignore
文件,请在与 Gemfile
相同的级别创建一个 .gitignore
文件。
-
将 .env
添加到 .gitignore
文件中。这将防止您意外提交您的客户端密钥。有关 .gitignore
文件的更多信息,请参阅“忽略文件”。
-
将以下内容添加到您的 .env
文件中。用您应用的客户端 ID 替换 YOUR_CLIENT_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
的顶部,添加这些依赖项
Rubyrequire "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 和客户端密钥。
RubyCLIENT_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
,以显示一个链接,该链接将提示用户验证您的应用。
Rubyget "/" 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 是 https://127.0.0.1:4567/github/callback
,请将 CALLBACK_URL
替换为 /github/callback
。
Rubyget "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,减去域名
Rubyrequire "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 服务器应启动。
-
在您的浏览器中,导航到 https://127.0.0.1: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
以交换代码参数以获取用户访问令牌。
- 回调页面现在显示文本以指示已生成令牌。如果令牌生成不成功,该页面将指示该失败。
Rubyrequire "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 服务器应启动。
- 在您的浏览器中,导航到
https://127.0.0.1:4567
。您应该会看到一个文本为“使用 GitHub 登录”的链接。
- 点击“使用 GitHub 登录”链接。
- 在提示时,授权您的应用。
- 通过单击“使用 GitHub 登录”链接,然后在提示时授权该应用,可以到达回调 URL 页面,该页面应显示类似于“成功授权!获取代码 4acd44861aeda86dacce 并将其交换为以 2zU5kQziE 结尾的用户访问令牌”的文本。
- 在 Sinatra 正在运行的终端中,通过输入 Ctrl+C 停止服务器。
-
现在您已拥有用户访问令牌,您可以使用该令牌代表用户发出 API 请求。例如
将此函数添加到 app.rb
以获取有关使用 /user
REST API 端点获取用户信息的信息
Rubydef 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
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,不包括域名。
Rubyget "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 是 https://127.0.0.1:4567/github/callback
,则将 CALLBACK_URL
替换为 /github/callback
。
Rubyrequire "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
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
。
-
在您的终端中,从存储 app.rb
的目录中,运行 ruby app.rb
。本地 Sinatra 服务器应启动。
-
在您的浏览器中,导航到 https://127.0.0.1:4567
。您应该会看到一个文本为“使用 GitHub 登录”的链接。
-
点击“使用 GitHub 登录”链接。
如果您尚未授权该应用,点击该链接应会将您带到 https://github.com/login/oauth/authorize?client_id=CLIENT_ID
,其中 CLIENT_ID
是您应用的客户端 ID。这是一个 GitHub 页面,提示用户授权您的应用。如果您点击按钮以授权您的应用,您将转到应用的回调 URL。
如果您之前已授权您的应用并且授权尚未被撤销,您将跳过授权提示并直接转到回调 URL。如果您想看到授权提示,您可以撤销您之前的授权。有关更多信息,请参阅“查看和撤销 GitHub 应用的授权”。
-
回调 URL 页面,通过点击“使用 GitHub 登录”链接,然后在提示时授权该应用即可访问,应显示类似于“授权成功!欢迎,Mona Lisa (octocat)。”的文本。
-
在 Sinatra 正在运行的终端中,通过输入 Ctrl+C 停止服务器。
你永远不应公开你的应用的客户端密钥。本教程将客户端密钥存储在被 gitignore 的 .env
文件中,并使用 ENV.fetch
访问该值。当你部署你的应用时,你应选择一种安全的方式来存储客户端密钥,并相应地更新你的代码以获取该值。
例如,你可以将密钥存储在应用部署到的服务器上的环境变量中。你还可以使用秘密管理服务,如 Azure Key Vault。
本教程使用了一个以 https://127.0.0.1:4567
开头的回调 URL。但是,https://127.0.0.1:4567
仅在你启动 Sinatra 服务器时在本地计算机上可用。在部署你的应用之前,你应更新回调 URL 以使用你在生产中使用的回调 URL。有关更新应用的回调 URL 的更多信息,请参阅“修改 GitHub 应用注册”和“关于用户授权回调 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 应用的最佳实践”。