简介
本教程演示如何构建一个由 GitHub 应用支持的命令行界面 (CLI),以及如何使用设备流为应用生成用户访问令牌。
CLI 将有三个命令
help
:输出使用说明。login
:生成用户访问令牌,应用可以使用该令牌代表用户进行 API 请求。whoami
:返回有关已登录用户的信息。
本教程使用 Ruby,但您可以使用任何编程语言编写 CLI 并使用设备流生成用户访问令牌。
关于设备流和用户访问令牌
CLI 将使用设备流对用户进行身份验证并生成用户访问令牌。然后,CLI 可以使用用户访问令牌代表已验证用户进行 API 请求。
如果要将应用的操作归因于用户,则应用应使用用户访问令牌。有关更多信息,请参阅“代表用户使用 GitHub 应用进行身份验证”。
为 GitHub App 生成用户访问令牌有两种方法:Web 应用程序流程和设备流程。如果您的应用程序是无头或无法访问 Web 界面,则应使用设备流程生成用户访问令牌。例如,CLI 工具、简单的 Raspberry Pi 和桌面应用程序应使用设备流程。如果您的应用程序可以访问 Web 界面,则应使用 Web 应用程序流程。有关更多信息,请参阅 "为 GitHub App 生成用户访问令牌" 和 "使用 GitHub App 构建“使用 GitHub 登录”按钮"。
先决条件
本教程假设您已经注册了 GitHub App。有关注册 GitHub App 的更多信息,请参阅 "注册 GitHub App"。
在按照本教程操作之前,您必须为您的应用程序启用设备流程。有关为您的应用程序启用设备流程的更多信息,请参阅 "修改 GitHub App 注册"。
本教程假设您对 Ruby 有基本了解。有关更多信息,请参阅 Ruby.
获取客户端 ID
您需要您的应用程序的客户端 ID 才能通过设备流程生成用户访问令牌。
- 在 GitHub 上任何页面的右上角,单击您的个人资料照片。
- 导航到您的帐户设置。
- 对于个人帐户拥有的应用程序,请单击 **设置**。
- 对于组织拥有的应用程序
- 单击 **您的组织**。
- 在组织的右侧,单击 **设置**。
- 在左侧边栏中,单击 ** 开发者设置**。
- 在左侧边栏中,单击 **GitHub Apps**。
- 在您要使用的 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 App 生成用户访问令牌”。
-
在文件顶部,在
require
语句之后,在app_cli.rb
中添加 GitHub App 的CLIENT_ID
作为常量。有关查找应用程序客户端 ID 的更多信息,请参阅“获取客户端 ID”。将YOUR_CLIENT_ID
替换为应用程序的客户端 ID。Ruby 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 应用程序的最佳实践。有关更多信息,请参阅“创建 GitHub 应用程序的最佳实践”。