简介
本教程演示如何生成由 GitHub App 提供支持的命令行接口 (CLI),以及如何使用设备流为应用生成用户访问令牌。
CLI 将有三个命令:
help
:输出使用说明。login
:生成一个用户访问令牌,应用可以使用该令牌代表用户发出 API 请求。whoami
:返回有关登录用户的信息。
本教程使用 Ruby,但你可以编写 CLI 并使用设备流通过任何编程语言生成用户访问令牌。
关于设备流和用户访问令牌
CLI 将使用设备流对用户进行身份验证并生成用户访问令牌。 然后,CLI 可以使用用户访问令牌代表经过身份验证的用户发出 API 请求。
如果你想要将应用操作归因于用户,应用应使用用户访问令牌。 有关详细信息,请参阅“代表用户使用 GitHub 应用进行身份验证”。
可通过两种方式为 GitHub App 生成用户访问令牌:Web 应用程序流和设备流。 如果应用无外设应用或无权访问 Web 接口,你应使用设备流来生成用户访问令牌。 例如,CLI 工具、简单的 Raspberry Pi 和桌面应用程序应使用设备流。 如果应用有权访问 Web 接口,则应改用 Web 应用程序流。 有关详细信息,请参阅 为 GitHub 应用生成用户访问令牌 和 使用 GitHub Apps 生成“使用 GitHub 登录”按钮。
先决条件
本教程假定你已注册 GitHub App。 若要详细了解如何注册 GitHub App,请参阅“注册 GitHub 应用”。
在按照本教程操作之前,必须为应用启用设备流。 有关为应用启用设备流的详细信息,请参阅“修改 GitHub 应用注册”。
本教程假定你对 Ruby 有基本的了解。 有关详细信息,请参阅 Ruby。
获取客户端 ID
需要应用的客户端 ID 才能通过设备流生成用户访问令牌。
- 在 GitHub 上任意页的右上角,单击你的个人资料照片。
- 导航到你的帐户设置。
- 对于由个人帐户拥有的应用,请单击“设置”****。
- 对于组织拥有的应用:
- 单击“你的组织”。
- 在组织的右侧,单击设置。
- 在左侧边栏中,单击“ 开发人员设置”。
- 在左侧边栏中,单击“GitHub Apps”。
- 在要使用的 GitHub App 旁边,单击“编辑”。
- 在应用的“设置”页上,找到应用的客户端 ID。 本教程后面部分将使用它。 请注意,客户端 ID 不同于应用程序 ID。
编写 CLI
这些步骤将引导你生成 CLI 并使用设备流获取用户访问令牌。 若要跳到最终代码,请参阅完整代码示例。
安装
-
创建 Ruby 文件以保存将生成用户访问令牌的代码。 本教程将该文件命名为
app_cli.rb
。 -
在终端中,从存储
app_cli.rb
的目录中运行以下命令,使app_cli.rb
可执行:Text 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
命令
-
向
app_cli.rb
添加以下help
函数。 目前,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
语句之后,将 GitHub App 的CLIENT_ID
添加为app_cli.rb
中的常量。 有关查找应用客户端 ID 的详细信息,请参阅获取客户端 ID。 将YOUR_CLIENT_ID
替换为应用的客户端 ID:Ruby CLIENT_ID="YOUR_CLIENT_ID"
CLIENT_ID="YOUR_CLIENT_ID"
-
向
app_cli.rb
添加以下parse_response
函数。 此函数分析来自 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
-
向
app_cli.rb
添加以下request_device_code
函数。 此函数向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
-
向
app_cli.rb
添加以下request_token
函数。 此函数向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
-
向
app_cli.rb
添加以下poll_for_token
函数。 此函数按指定间隔轮询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
命令以获取经过身份验证的用户的用户名。
-
向
app_cli.rb
添加以下whoami
函数。 此函数获取有关使用/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 Apps 选择权限”。
安全地存储令牌
本教程将生成用户访问令牌并将其保存在本地文件中。 切勿提交此文件或公开令牌。
根据设备,可以选择不同的方法来存储令牌。 应检查在设备上存储令牌的最佳做法。
有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。
遵循最佳做法
你的目标应该是遵循 GitHub App 的最佳做法。 有关详细信息,请参阅“创建 GitHub 应用的最佳做法”。