简介
本教程演示如何生成由 GitHub App 提供支持的命令行接口 (CLI),以及如何使用设备流为应用生成用户访问令牌。
CLI 将有三个命令:
help
:输出使用说明。login
:生成一个用户访问令牌,应用可以使用该令牌代表用户发出 API 请求。whoami
:返回有关登录用户的信息。
本教程使用 Ruby,但你可以编写 CLI 并使用设备流通过任何编程语言生成用户访问令牌。
Note
本文包含使用 github.com
域的命令或示例。 可以在其他域(例如 octocorp.ghe.com
)中访问 GitHub。
关于设备流和用户访问令牌
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 上任意页的右上角,单击你的个人资料照片。
-
导航到你的帐户设置。
- 对于由个人帐户拥有的应用,请单击“设置”****。
- 对于组织拥有的应用:
- 单击“你的组织”。
- 在组织的右侧,单击设置。
- 对于由企业拥有的应用:
- 如果使用的是 Enterprise Managed Users,请单击你的企业,以直接转到企业帐户设置。
- 如果使用的是个人帐户,请单击你的企业,然后单击企业右侧的设置。
-
导航到 GitHub App 设置。
- 对于由个人帐户或组织拥有的应用:
- 在左侧边栏中,单击 开发人员设置,然后单击 GitHub Apps。
- 对于由企业拥有的应用:
- 在左侧边栏中,在“设置”下,单击 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 应用的最佳做法”。