Skip to main content

此版本的 GitHub Enterprise Server 将于以下日期停止服务 2026-03-17. 即使针对重大安全问题,也不会发布补丁。 为了获得更好的性能、更高的安全性和新功能,请升级到最新版本的 GitHub Enterprise。 如需升级帮助,请联系 GitHub Enterprise 支持

使用 GitHub 应用生成 CLI

按照本教程操作,在 Ruby 中编写一个 CLI,该 CLI 通过设备流为 GitHub App 生成用户访问令牌。

简介

本教程演示如何生成由 GitHub App 提供支持的命令行接口 (CLI),以及如何使用设备流为应用生成用户访问令牌。

CLI 将有三个命令:

  • :输出使用说明。
  • :生成一个用户访问令牌,应用可以使用该令牌代表用户发出 API 请求。
  • :返回有关登录用户的信息。

本教程使用 Ruby,但你可以编写 CLI 并使用设备流通过任何编程语言生成用户访问令牌。

关于设备流和用户访问令牌

CLI 将使用设备流对用户进行身份验证并生成用户访问令牌。 然后,CLI 可以使用用户访问令牌代表经过身份验证的用户发出 API 请求。

如果你想要将应用操作归因于用户,应用应使用用户访问令牌。 有关详细信息,请参阅“AUTOTITLE”。

可通过两种方式为 GitHub App 生成用户访问令牌:Web 应用程序流和设备流。 如果应用是无界面应用或无法访问 Web 接口,你应使用设备流来生成用户访问令牌。 例如,CLI 工具、简单的 Raspberry Pi 和桌面应用程序应使用设备流。 如果应用有权访问 Web 接口,则应改用 Web 应用流。 有关详细信息,请参阅 AUTOTITLE 和 AUTOTITLE。

先决条件

本教程假定你已注册 GitHub App。 如果您想了解如何注册 GitHub App,请参阅 AUTOTITLE。

在按照本教程操作之前,必须为应用启用设备流。 有关为应用启用设备流的详细信息,请参阅“AUTOTITLE”。

本教程假定你对 Ruby 有基本的了解。 有关详细信息,请参阅 Ruby。

获取客户端 ID

需要应用的客户端 ID 才能通过设备流生成用户访问令牌。

  1. 在 GitHub 上任意页的右上角,单击你的个人资料图片。
  2. 导航到你的帐户设置。
    • 对于由个人帐户拥有的应用,请单击“设置”****。
    • 对于组织拥有的应用:
      1. 单击“你的组织”。
      2. 在组织的右侧,单击设置。 1. 在左边栏中,单击 “Developer settings”****。
  3. 在左侧边栏中,单击“GitHub Apps”。
  4. 在要使用的 GitHub App 旁边,单击“编辑”。
  5. 在应用的“设置”页上,找到应用的客户端 ID。 本教程后面部分将使用它。 请注意,客户端 ID 不同于应用程序 ID。

编写命令行界面(CLI)

这些步骤将引导你生成 CLI 并使用设备流获取用户访问令牌。 若要跳到最终代码,请参阅完整代码示例。

安装

  1. 创建 Ruby 文件以保存将生成用户访问令牌的代码。 本教程将该文件命名为 。

  2. 在终端中,从存储 的目录中运行以下命令,使 可执行:

    Text
    chmod +x app_cli.rb
    
  3. 将以下行添加到 顶部,以指示应使用 Ruby 解释器运行脚本:

    Ruby
    #!/usr/bin/env ruby
    
  4. 将这些依赖项添加到 顶部,如下所示 :

    Ruby
    require "net/http"
    require "json"
    require "uri"
    require "fileutils"
    

    这些都是 Ruby 标准库的一部分,因此无需安装任何 gem。

  5. 添加下面的 函数作为入口点。 该函数包含一个 语句,用于根据指定的命令执行不同操作。 稍后您将展开此语句。

    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
    
  6. 在文件底部,添加以下行以调用入口点函数。 在本教程后面向此文件添加更多函数时,此函数调用应保留在文件底部。

    Ruby
    main
    
  7. (可选)检查进度:

    现在看起来是这样的:

    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
    

    在终端中,从存储 的目录中运行 。 你应该会看到以下输出:

    `help` is not yet defined
    

    还可以在不使用命令或使用未经处理的命令的情况下测试脚本。 例如, 应输出:

    Unknown command `create-issue`
    

添加 命令

  1. 向 添加以下的函数。 目前, 函数会打印一行,告知用户此 CLI 需要一个命令“help”。 稍后将扩展此函数。

    Ruby
    def help
      puts "usage: app_cli <help>"
    end
    
  2. 更新 函数,以在给定 命令时调用 函数:

    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
    
  3. (可选)检查进度:

    现在看起来是这样的。 只要 函数调用位于文件末尾,函数的顺序就无关紧要。

    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
    

    在终端中,从存储 的目录中运行 。 你应该会看到以下输出:

    usage: app_cli <help>
    

添加 命令

命令将运行设备流以获取用户访问令牌。 有关详细信息,请参阅“AUTOTITLE”。

  1. 在文件顶部附近,在 语句之后,将 GitHub App 的 添加为 中的常量。 有关查找应用客户端 ID 的详细信息,请参阅获取客户端 ID。 将 替换为应用的客户端 ID:

    Ruby
    CLIENT_ID="YOUR_CLIENT_ID"
    
  2. 向 添加以下 函数。 此函数分析来自 GitHub REST API 的响应。 当响应状态为 或 时,函数将返回分析的响应正文。 否则,函数将输出响应和正文,并退出程序。

    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
    
  3. 向 添加以下 函数。 此函数向 发出 请求并返回响应。

    Ruby
    def request_device_code
      uri = URI("http(s)://HOSTNAME/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
    
  4. 向 添加以下 函数。 此函数向 发出 请求并返回响应。

    Ruby
    def request_token(device_code)
      uri = URI("http(s)://HOSTNAME/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
    
  5. 向……添加以下函数。 此函数按指定的间隔进行轮询,直到 GitHub 使用某个参数响应,而不是使用另一个参数响应。 然后,它将用户访问令牌写入文件并限制该文件的权限。

    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
    
  6. 添加以下 函数。

    此函数:

    1. 调用 函数并从响应中获取 、、 和 参数。
    2. 提示用户输入上一步中的 。
    3. 调用 轮询 GitHub 以获取访问令牌。
    4. 让用户知道身份验证成功。
    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
    
  7. 更新 函数,以在给定 命令时调用 函数:

    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
    
  8. 更新 函数以包含 命令:

    Ruby
    def help
      puts "usage: app_cli <login | help>"
    end
    
  9. (可选)检查进度:

    现在如下所示,其中 是应用的客户端 ID。 只要 函数调用位于文件末尾,函数的顺序就无关紧要。

    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("http(s)://HOSTNAME/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("http(s)://HOSTNAME/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
    
    1. 在终端中,从存储 的目录中运行 。 应该会看到如下所示的输出。 每次代码都会有所不同:

      Please visit: http(s)://HOSTNAME/login/device
      and enter code: CA86-8D94
      
    2. 在浏览器中导航到 http(s)://HOSTNAME/login/device,输入上一步中的代码,然后单击“继续”。

    3. GitHub 应该会显示一个页面,提示你授权应用。 单击“授权”按钮。

    4. 终端现在应显示“已成功进行身份验证!”。

添加 命令

现在,应用可以生成用户访问令牌,你可以代表用户发出 API 请求。 添加 命令以获取经过身份验证的用户的用户名。

  1. 向……添加以下函数。 此函数获取有关使用 REST API 终结点的用户的信息。 它输出与用户访问令牌对应的用户名。 如果未找到 文件,它将提示用户运行 函数。

    Ruby
    def whoami
      uri = URI("http(s)://HOSTNAME/api/v3/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
    
  2. 更新 函数以处理令牌已过期或已吊销的情况。 现在,如果收到 响应,CLI 将提示用户运行 命令。

    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
    
  3. 更新 函数,以在给定 命令时调用 函数:

    Ruby
    def main
      case ARGV[0]
      when "help"
        help
      when "login"
        login
      when "whoami"
        whoami
      else
        puts "Unknown command #{ARGV[0]}"
      end
    end
    
  4. 更新 函数以包含 命令:

    Ruby
    def help
      puts "usage: app_cli <login | whoami | help>"
    end
    
  5. 请将您的代码与下一部分的完整示例代码进行比对。 可以按照完整代码示例下方测试一节中概述的步骤测试代码。

完整代码示例

这是上一部分概述的完整代码示例。 将 替换为应用的客户端 ID。

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 | 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("http(s)://HOSTNAME/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("http(s)://HOSTNAME/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("http(s)://HOSTNAME/api/v3/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

测试

本教程假定应用代码存储在名为 的文件中。

  1. 在终端中,从存储 的目录中运行 。 应该会看到如下所示的输出。

    usage: app_cli <login | whoami | help>
    
  2. 在终端中,从存储 的目录中运行 。 应该会看到如下所示的输出。 每次代码都会有所不同:

    Please visit: http(s)://HOSTNAME/login/device
    and enter code: CA86-8D94
    
  3. 在浏览器中导航到 http(s)://HOSTNAME/login/device,输入上一步中的代码,然后单击“继续”。

  4. GitHub 应该会显示一个页面,提示你授权应用。 单击“授权”按钮。

  5. 终端现在应显示“已成功进行身份验证!”。

  6. 在终端中,从存储 的目录中运行 。 你应该看到类似这样的输出,其中是你的用户名。

    You are octocat
    
  7. 在编辑器中打开 文件,并修改令牌。 现在,令牌无效。

  8. 在终端中,从存储 的目录中运行 。 应该会看到如下所示的输出:

    You are not authorized. Run the `login` command.
    
  9. 删除 文件,

  10. 在终端中,从存储 的目录中运行 。 应该会看到如下所示的输出:

    You are not authorized. Run the `login` command.
    

后续步骤

调整代码以满足应用的需求

本教程演示如何编写使用设备流生成用户访问令牌的 CLI。 可以展开此 CLI 以接受其他命令。 例如,您可以添加一个命令来打开一个问题。 针对你要发出的 API 请求,如果应用需要其他权限,请记得更新应用的权限。 有关详细信息,请参阅“AUTOTITLE”。

安全地存储令牌

本教程将生成用户访问令牌并将其保存在本地文件中。 切勿提交此文件或公开令牌。

根据设备,可以选择不同的方法来存储令牌。 应检查在设备上存储令牌的最佳做法。

有关详细信息,请参阅“AUTOTITLE”。

遵循最佳做法

你的目标应该是遵循 GitHub App 的最佳做法。 有关详细信息,请参阅“AUTOTITLE”。