简介
本教程演示如何为网站构建“使用 GitHub 登录”按钮。该网站将使用 GitHub 应用通过 Web 应用流程生成用户访问令牌。然后,网站使用用户访问令牌代表已认证的用户发出 API 请求。
本教程使用 Ruby,但您可以将 Web 应用流程与用于 Web 开发的任何编程语言一起使用。
关于 Web 应用流程和用户访问令牌
如果要将应用的操作归因于用户,则您的应用应使用用户访问令牌。有关更多信息,请参阅“代表用户使用 GitHub 应用进行身份验证”。
有两种方法可以为 GitHub 应用生成用户访问令牌:Web 应用流程和设备流程。如果您的应用可以访问 Web 界面,则应使用 Web 应用流程。如果您的应用无法访问 Web 界面,则应改用设备流程。有关更多信息,请参阅“为 GitHub 应用生成用户访问令牌”和“使用 GitHub 应用构建 CLI”。
先决条件
本教程假设您已注册了一个 GitHub 应用。有关注册 GitHub 应用的更多信息,请参阅“注册 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。例如,您可以使用Bundler来完成此操作。
-
如果您尚未安装 Bundler,请在终端中运行以下命令
gem install bundler
-
如果您尚未为您的应用创建 Gemfile,请在终端中运行以下命令
bundle init
-
如果您尚未为您的应用创建 Gemfile.lock,请在终端中运行以下命令
bundle install
-
通过在终端中运行以下命令安装 gem
bundle add sinatra
bundle add dotenv
存储客户端 ID 和客户端密钥
本教程将向您展示如何将客户端 ID 和客户端密钥存储在环境变量中,并使用 ENV.fetch
访问它们。部署应用时,您需要更改存储客户端 ID 和客户端密钥的方式。有关更多信息,请参阅“安全存储您的客户端密钥”。
-
在 GitHub 上任何页面右上角,点击您的个人资料照片。
-
导航到您的帐户设置。
- 对于个人帐户拥有的应用,请点击“**设置**”。
- 对于组织拥有的应用
- 点击“**您的组织**”。
- 在组织右侧,点击“**设置**”。
-
在左侧边栏中,点击“** 开发者设置**”。
-
在左侧边栏中,点击“**GitHub 应用**”。
-
在要使用的 GitHub 应用旁边,点击“**编辑**”。
-
在应用的设置页面上,查找应用的客户端 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 登录”链接。
- 如果系统提示,请授权您的应用。
- 回调 URL 页面(通过点击“使用 GitHub 登录”链接并在系统提示时授权应用访问)应显示类似于“已成功授权!获取代码 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| 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,减去域名。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| 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 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 应用的授权”。
-
回调 URL 页面(通过点击“使用 GitHub 登录”链接并在系统提示时授权应用访问)应显示类似于“已成功授权!欢迎,Mona Lisa (octocat)。 ”的文本。
-
在运行 Sinatra 的终端中,通过输入 Ctrl+C 停止服务器。
后续步骤
安全存储您的客户端密钥
您永远不应该公开您的应用的客户端密钥。本教程将客户端密钥存储在 .gitignore 文件 .env
中,并使用 ENV.fetch
访问其值。部署应用时,您应该选择一种安全的方式来存储客户端密钥,并更新您的代码以相应地获取该值。
例如,您可以将密钥存储在部署应用程序的服务器上的环境变量中。您还可以使用 Azure Key Vault 等密钥管理服务。
更新部署的回调 URL
本教程使用了以 http://localhost:4567
开头的回调 URL。但是,http://localhost: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 应用的最佳实践”。