简介
本教程演示如何构建由 GitHub 应用支持的命令行界面 (CLI),以及如何使用设备流为应用生成用户访问令牌。
CLI 将具有三个命令
help
:输出使用说明。login
:生成应用可用于代表用户发出 API 请求的用户访问令牌。whoami
:返回有关已登录用户的信息。
本教程使用 Ruby,但您可以使用任何编程语言编写 CLI 并使用设备流生成用户访问令牌。
关于设备流和用户访问令牌
CLI 将使用设备流对用户进行身份验证并生成用户访问令牌。然后,CLI 可以使用用户访问令牌代表经过身份验证的用户发出 API 请求。
如果要将应用的操作归因于用户,则您的应用应使用用户访问令牌。有关更多信息,请参阅“代表用户使用 GitHub 应用进行身份验证”。
有两种方法可以为 GitHub 应用生成用户访问令牌:Web 应用流程和设备流。如果您的应用是无头的或无法访问 Web 界面,则应使用设备流生成用户访问令牌。例如,CLI 工具、简单的树莓派和桌面应用程序应使用设备流。如果您的应用可以访问 Web 界面,则应改为使用 Web 应用流程。有关更多信息,请参阅“为 GitHub 应用生成用户访问令牌”和“使用 GitHub 应用构建“使用 GitHub 登录”按钮”。
先决条件
本教程假设您已注册了一个 GitHub 应用。有关注册 GitHub 应用的更多信息,请参阅“注册 GitHub 应用”。
在按照本教程操作之前,您必须为您的应用启用设备流。有关为您的应用启用设备流的更多信息,请参阅“修改 GitHub 应用注册”。
本教程假设您具备 Ruby 的基本知识。有关更多信息,请参阅 Ruby。
获取客户端 ID
您需要应用的客户端 ID 才能通过设备流生成用户访问令牌。
-
在 GitHub 上任何页面右上角,点击您的个人资料照片。
-
导航到您的帐户设置。
- 对于个人帐户拥有的应用,点击“**设置**”。
- 对于组织拥有的应用
- 点击“**您的组织**”。
- 在组织右侧,点击“**设置**”。
-
在左侧边栏中,点击“ 开发者设置”。
-
在左侧边栏中,点击“**GitHub 应用**”。
-
在您要使用的 GitHub 应用旁边,点击“**编辑**”。
-
在应用的设置页面上,找到您的应用的客户端 ID。您将在本教程后面的步骤中使用它。请注意,客户端 ID 与应用 ID 不同。
编写 CLI
这些步骤将引导您构建 CLI 并使用设备流获取用户访问令牌。要跳至最终代码,请参阅“完整代码示例”。
设置
-
创建一个 Ruby 文件来保存将生成用户访问令牌的代码。本教程将该文件命名为
app_cli.rb
。 -
在您的终端中,从存储
app_cli.rb
的目录运行以下命令,使app_cli.rb
可执行文本 chmod +x app_cli.rb
chmod +x app_cli.rb
-
将此行添加到
app_cli.rb
的顶部,以指示应使用 Ruby 解释器运行脚本Ruby #!/usr/bin/env ruby
#!/usr/bin/env ruby
-
将这些依赖项添加到
app_cli.rb
的顶部,位于#!/usr/bin/env ruby
之后Ruby require "net/http" require "json" require "uri" require "fileutils"
require "net/http" require "json" require "uri" require "fileutils"
这些都是 Ruby 标准库的一部分,因此您无需安装任何 gem。
-
添加以下
main
函数,该函数将用作入口点。该函数包含一个case
语句,根据指定哪个命令采取不同的操作。您将在稍后扩展此case
语句。Ruby def main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end end
def main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end end
-
在文件的底部,添加以下行以调用入口点函数。在您在本教程后面的步骤中向此文件添加更多函数时,此函数调用应保留在文件的底部。
Ruby main
main
-
可选:检查您的进度
app_cli.rb
现在看起来像这样Ruby #!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end end main
#!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def main case ARGV[0] when "help" puts "`help` is not yet defined" when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command `#{ARGV[0]}`" end end main
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb help
。您应该看到以下输出`help` is not yet defined
您也可以在没有命令或使用未处理的命令的情况下测试您的脚本。例如,
./app_cli.rb create-issue
应该输出Unknown command `create-issue`
添加 help
命令
-
将以下
help
函数添加到app_cli.rb
。目前,help
函数打印一行,告诉用户此 CLI 接受一个命令“help”。您将在稍后扩展此help
函数。Ruby def help puts "usage: app_cli <help>" end
def help puts "usage: app_cli <help>" end
-
更新
main
函数,以便在给出help
命令时调用help
函数Ruby def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end
def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end
-
可选:检查您的进度
app_cli.rb
现在看起来像这样。只要main
函数调用位于文件的末尾,函数的顺序就无关紧要。Ruby #!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def help puts "usage: app_cli <help>" end def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end main
#!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" def help puts "usage: app_cli <help>" end def main case ARGV[0] when "help" help when "login" puts "`login` is not yet defined" when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end main
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb help
。您应该看到以下输出usage: app_cli <help>
添加 login
命令
login
命令将运行设备流以获取用户访问令牌。有关更多信息,请参阅“为 GitHub 应用生成用户访问令牌”。
-
在文件的顶部,
require
语句之后,在app_cli.rb
中添加 GitHub 应用的CLIENT_ID
作为常量。有关查找应用的客户端 ID 的更多信息,请参阅“获取客户端 ID”。将YOUR_CLIENT_ID
替换为您的应用的客户端 IDRuby CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_ID="YOUR_CLIENT_ID"
-
将以下
parse_response
函数添加到app_cli.rb
。此函数解析来自 GitHub REST API 的响应。当响应状态为200 OK
或201 Created
时,该函数将返回解析的响应正文。否则,该函数将打印响应和正文并退出程序。Ruby def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end end
def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end end
-
将以下
request_device_code
函数添加到app_cli.rb
。此函数向https://github.com/login/device/code
发出POST
请求并返回响应。Ruby def request_device_code uri = URI("https://github.com/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end
def request_device_code uri = URI("https://github.com/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end
-
将以下
request_token
函数添加到app_cli.rb
。此函数向https://github.com/login/oauth/access_token
发出POST
请求并返回响应。Ruby def request_token(device_code) uri = URI("https://github.com/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end
def request_token(device_code) uri = URI("https://github.com/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end
-
将以下
poll_for_token
函数添加到app_cli.rb
。此函数以指定的间隔轮询https://github.com/login/oauth/access_token
,直到 GitHub 返回包含access_token
参数而不是error
参数的响应。然后,它将用户访问令牌写入文件并限制文件的权限。Ruby def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end
def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end
-
添加以下
login
函数。此函数
- 调用
request_device_code
函数并从响应中获取verification_uri
、user_code
、device_code
和interval
参数。 - 提示用户输入上一步中的
user_code
。 - 调用
poll_for_token
以轮询 GitHub 以获取访问令牌。 - 让用户知道身份验证已成功。
Ruby def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end
def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end
- 调用
-
更新
main
函数,以便在给出login
命令时调用login
函数Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end
def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end
-
更新
help
函数以包含login
命令Ruby def help puts "usage: app_cli <login | help>" end
def help puts "usage: app_cli <login | help>" end
-
可选:检查您的进度
app_cli.rb
现在看起来类似于此,其中YOUR_CLIENT_ID
是您的应用的客户端 ID。只要main
函数调用位于文件的末尾,函数的顺序就无关紧要。Ruby #!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" CLIENT_ID="YOUR_CLIENT_ID" def help puts "usage: app_cli <login | help>" end def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end end def request_device_code uri = URI("https://github.com/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def request_token(device_code) uri = URI("https://github.com/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end main
#!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" CLIENT_ID="YOUR_CLIENT_ID" def help puts "usage: app_cli <login | help>" end def main case ARGV[0] when "help" help when "login" login when "whoami" puts "`whoami` is not yet defined" else puts "Unknown command #{ARGV[0]}" end end def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) else puts response puts response.body exit 1 end end def request_device_code uri = URI("https://github.com/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def request_token(device_code) uri = URI("https://github.com/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end main
-
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb login
。您应该看到如下所示的输出。代码每次都会有所不同Please visit: https://github.com/login/device and enter code: CA86-8D94
-
在浏览器中导航到 https://github.com/login/device 并输入上一步中的代码,然后点击“**继续**”。
-
GitHub 应该会显示一个页面,提示您授权您的应用。点击“**授权**”按钮。
-
您的终端现在应该显示“身份验证成功!”。
-
添加 whoami
命令
现在您的应用可以生成用户访问令牌了,您可以代表用户发出 API 请求。添加一个 whoami
命令以获取经过身份验证的用户名的用户名。
-
将以下
whoami
函数添加到app_cli.rb
。此函数使用/user
REST API 端点获取有关用户的信息。它输出与用户访问令牌相对应的用户名。如果未找到.token
文件,它将提示用户运行login
函数。Ruby def whoami uri = URI("https://api.github.com/user") begin token = File.read("./.token").strip rescue Errno::ENOENT => e puts "You are not authorized. Run the `login` command." exit 1 end response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"} http.send_request("GET", uri.path, body, headers) end parsed_response = parse_response(response) puts "You are #{parsed_response["login"]}" end
def whoami uri = URI("https://api.github.com/user") begin token = File.read("./.token").strip rescue Errno::ENOENT => e puts "You are not authorized. Run the `login` command." exit 1 end response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"} http.send_request("GET", uri.path, body, headers) end parsed_response = parse_response(response) puts "You are #{parsed_response["login"]}" end
-
更新
parse_response
函数以处理令牌已过期或被撤销的情况。现在,如果您收到401 Unauthorized
响应,CLI 将提示用户运行login
命令。Ruby def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) when Net::HTTPUnauthorized puts "You are not authorized. Run the `login` command." exit 1 else puts response puts response.body exit 1 end end
def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) when Net::HTTPUnauthorized puts "You are not authorized. Run the `login` command." exit 1 else puts response puts response.body exit 1 end end
-
更新
main
函数,以便在给出whoami
命令时调用whoami
函数Ruby def main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end end
def main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end end
-
更新
help
函数以包含whoami
命令Ruby def help puts "usage: app_cli <login | whoami | help>" end
def help puts "usage: app_cli <login | whoami | help>" end
-
将您的代码与下一节中的完整代码示例进行比较。您可以按照完整代码示例下方的“测试”部分中概述的步骤测试您的代码。
完整代码示例
这是上一节中概述的完整代码示例。将 YOUR_CLIENT_ID
替换为您的应用的客户端 ID。
#!/usr/bin/env ruby require "net/http" require "json" require "uri" require "fileutils" CLIENT_ID="YOUR_CLIENT_ID" def help puts "usage: app_cli <login | whoami | help>" end def main case ARGV[0] when "help" help when "login" login when "whoami" whoami else puts "Unknown command #{ARGV[0]}" end end def parse_response(response) case response when Net::HTTPOK, Net::HTTPCreated JSON.parse(response.body) when Net::HTTPUnauthorized puts "You are not authorized. Run the `login` command." exit 1 else puts response puts response.body exit 1 end end def request_device_code uri = URI("https://github.com/login/device/code") parameters = URI.encode_www_form("client_id" => CLIENT_ID) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def request_token(device_code) uri = URI("https://github.com/login/oauth/access_token") parameters = URI.encode_www_form({ "client_id" => CLIENT_ID, "device_code" => device_code, "grant_type" => "urn:ietf:params:oauth:grant-type:device_code" }) headers = {"Accept" => "application/json"} response = Net::HTTP.post(uri, parameters, headers) parse_response(response) end def poll_for_token(device_code, interval) loop do response = request_token(device_code) error, access_token = response.values_at("error", "access_token") if error case error when "authorization_pending" # The user has not yet entered the code. # Wait, then poll again. sleep interval next when "slow_down" # The app polled too fast. # Wait for the interval plus 5 seconds, then poll again. sleep interval + 5 next when "expired_token" # The `device_code` expired, and the process needs to restart. puts "The device code has expired. Please run `login` again." exit 1 when "access_denied" # The user cancelled the process. Stop polling. puts "Login cancelled by user." exit 1 else puts response exit 1 end end File.write("./.token", access_token) # Set the file permissions so that only the file owner can read or modify the file FileUtils.chmod(0600, "./.token") break end end def login verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval") puts "Please visit: #{verification_uri}" puts "and enter code: #{user_code}" poll_for_token(device_code, interval) puts "Successfully authenticated!" end def whoami uri = URI("https://api.github.com/user") begin token = File.read("./.token").strip rescue Errno::ENOENT => e puts "You are not authorized. Run the `login` command." exit 1 end response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http| body = {"access_token" => token}.to_json headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"} http.send_request("GET", uri.path, body, headers) end parsed_response = parse_response(response) puts "You are #{parsed_response["login"]}" end main
#!/usr/bin/env ruby
require "net/http"
require "json"
require "uri"
require "fileutils"
CLIENT_ID="YOUR_CLIENT_ID"
def help
puts "usage: app_cli <login | whoami | help>"
end
def main
case ARGV[0]
when "help"
help
when "login"
login
when "whoami"
whoami
else
puts "Unknown command #{ARGV[0]}"
end
end
def parse_response(response)
case response
when Net::HTTPOK, Net::HTTPCreated
JSON.parse(response.body)
when Net::HTTPUnauthorized
puts "You are not authorized. Run the `login` command."
exit 1
else
puts response
puts response.body
exit 1
end
end
def request_device_code
uri = URI("https://github.com/login/device/code")
parameters = URI.encode_www_form("client_id" => CLIENT_ID)
headers = {"Accept" => "application/json"}
response = Net::HTTP.post(uri, parameters, headers)
parse_response(response)
end
def request_token(device_code)
uri = URI("https://github.com/login/oauth/access_token")
parameters = URI.encode_www_form({
"client_id" => CLIENT_ID,
"device_code" => device_code,
"grant_type" => "urn:ietf:params:oauth:grant-type:device_code"
})
headers = {"Accept" => "application/json"}
response = Net::HTTP.post(uri, parameters, headers)
parse_response(response)
end
def poll_for_token(device_code, interval)
loop do
response = request_token(device_code)
error, access_token = response.values_at("error", "access_token")
if error
case error
when "authorization_pending"
# The user has not yet entered the code.
# Wait, then poll again.
sleep interval
next
when "slow_down"
# The app polled too fast.
# Wait for the interval plus 5 seconds, then poll again.
sleep interval + 5
next
when "expired_token"
# The `device_code` expired, and the process needs to restart.
puts "The device code has expired. Please run `login` again."
exit 1
when "access_denied"
# The user cancelled the process. Stop polling.
puts "Login cancelled by user."
exit 1
else
puts response
exit 1
end
end
File.write("./.token", access_token)
# Set the file permissions so that only the file owner can read or modify the file
FileUtils.chmod(0600, "./.token")
break
end
end
def login
verification_uri, user_code, device_code, interval = request_device_code.values_at("verification_uri", "user_code", "device_code", "interval")
puts "Please visit: #{verification_uri}"
puts "and enter code: #{user_code}"
poll_for_token(device_code, interval)
puts "Successfully authenticated!"
end
def whoami
uri = URI("https://api.github.com/user")
begin
token = File.read("./.token").strip
rescue Errno::ENOENT => e
puts "You are not authorized. Run the `login` command."
exit 1
end
response = Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
body = {"access_token" => token}.to_json
headers = {"Accept" => "application/vnd.github+json", "Authorization" => "Bearer #{token}"}
http.send_request("GET", uri.path, body, headers)
end
parsed_response = parse_response(response)
puts "You are #{parsed_response["login"]}"
end
main
测试
本教程假设您的应用代码存储在名为 app_cli.rb
的文件中。
-
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb help
。您应该看到如下所示的输出。usage: app_cli <login | whoami | help>
-
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb login
。您应该看到如下所示的输出。代码每次都会有所不同Please visit: https://github.com/login/device and enter code: CA86-8D94
-
在浏览器中导航到 https://github.com/login/device 并输入上一步中的代码,然后点击“**继续**”。
-
GitHub 应该会显示一个页面,提示您授权您的应用。点击“**授权**”按钮。
-
您的终端现在应该显示“身份验证成功!”。
-
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb whoami
。您应该看到如下所示的输出,其中octocat
是您的用户名。You are octocat
-
在您的编辑器中打开
.token
文件,并修改令牌。现在,令牌无效。 -
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb whoami
。您应该看到如下所示的输出You are not authorized. Run the `login` command.
-
删除
.token
文件。 -
在您的终端中,从存储
app_cli.rb
的目录运行./app_cli.rb whoami
。您应该看到如下所示的输出You are not authorized. Run the `login` command.
后续步骤
调整代码以满足您的应用需求
本教程演示了如何编写一个使用设备流生成用户访问令牌的 CLI。您可以扩展此 CLI 以接受其他命令。例如,您可以添加一个 create-issue
命令来打开问题。请记住,如果您的应用需要您想要发出的 API 请求的其他权限,请更新您的应用的权限。有关更多信息,请参阅“为 GitHub 应用选择权限”。
安全地存储令牌
本教程生成用户访问令牌并将其保存在本地文件中。您永远不应提交此文件或公开令牌。
根据您的设备,您可以选择不同的方法来存储令牌。您应该检查在设备上存储令牌的最佳实践。
有关更多信息,请参阅“创建 GitHub 应用的最佳实践”。
遵循最佳实践
您应该尽量遵循 GitHub App 的最佳实践。有关更多信息,请参阅“创建 GitHub App 的最佳实践”。