使用Go语言执行GitHub GraphQL API(通过GitHub App认证)

本篇介绍了在使用Go语言执行GitHub GraphQL API时,认证处理的实现。认证使用了GitHub App的机制。

首先

您好,我是株式会社Kakukaku的工程师mamo。
这次在公司内部需要从Go语言调用GitHub GraphQL API,在参考GitHub官方页面等的基础上,尝试实现了一下,现在来给大家介绍一下实现的例子。
作者在调用GitHub API和初次体验golang方面可能还有些欠缺之处,请多多包涵。

前提 – 条件 – (literal translation)
假设 –

只需要一种选择 – Zhǐ

使用版本 go1.21.3 darwin/arm64
必须事先创建并安装了 GitHub 应用,并获取了 AppID,同时已经下载了密钥。

目标读者

针对想要在没有方便的库的情况下实现GitHub API认证的人。
我认为,如果使用像GitHub上已经公开的现有库,认证过程的实现就可以省去,但是我尝试了根据GitHub官方页面的内容进行实现,这是我尝试的结果。
此外,由于我还是第一次接触golang,所以我在结尾处写下了我的感想。

GitHub 的 GraphQL API?

在GitHub的官方页面上有一个名为「创建GraphQL调用」的页面,我将以此为基础来进行进一步的学习。

 

首先,GraphQL API有三种认证方法,包括Personal Access Token(PAT)、GitHub App和OAuth App。但是,考虑到本次需求是创建一个能够自动巡回公司内部仓库的爬虫应用,我们选择了GitHub App的认证方法。

由于PAT认证的问题,一个个人用户的权限关联会导致管理上的问题,并且如果用户帐户关闭,也有可能导致爬虫应用程序停止,因此我们在中途放弃了。

GraphQL的通信

如果使用curl命令,语法如下:

curl -H "Authorization: bearer TOKEN" -X POST -d " \
 { \
   \"query\": \"query { viewer { login }}\" \
 } \
" https://api.github.com/graphql

在这里指定的TOKEN不是JSON Web Token(JWT),而是指定“GitHub App安装认证”页面中提供的安装访问令牌。

 

安裝存取權杖

创建安装访问令牌需要一些步骤,并且稍微有点麻烦。

    1. 创建JWT。

 

    1. 使用创建的JWT令牌发送GET请求到/app/installations等,以获取安装ID。

 

    使用获取的安装ID(INSTALLATION_ID)发送POST请求到/app/installations/INSTALLATION_ID/access_tokens,以获取安装访问令牌。

在1小时内,您可以使用获取的安装访问令牌来调用GitHub的GraphQL API和REST API。

如果自己实现认证处理,就需要管理这种令牌的有效时间,当时间到期时,需要发行新的令牌并使用,这就需要进行生命周期管理。

本文实例中没有进行生命周期管理。

根据ChatGPT v4的说法,一旦获得了安装ID,只要不卸载和重新安装GitHub App,也不需要进行重新认证或权限更改,就可以将安装ID作为固定值并重复使用。

使用go语言进行实现的示例

GitHubAppID是指定预先创建的GitHub App的AppID。
GitHubAppPrivateKeyFile是指定GitHub App的私钥pem文件。
当GraphQL的执行结果为单一(非数组)或多个(数组)时,处理方式是临时应对的,因此对于此实现是否适当仍存有疑问。

package main

import (
	"encoding/json"
	"io"
	"net/http"
	"os"
	"strings"
	"time"

	"github.com/dgrijalva/jwt-go"
)

const (
	GitHubAppID             = "XXXXXX"
	GitHubAppPrivateKeyFile = "commitcrawler.2023-10-30.private-key.pem"
)

/* 汎用関数定義 */

// HTTPリクエストの実行
// (戻り値)
// JSONバイトデータ
func doHttp(req *http.Request) []byte {
	client := &http.Client{}
	resp, err := client.Do(req)
	if err != nil {
		panic(err)
	}
	defer resp.Body.Close()

	jsonByte, _ := io.ReadAll(resp.Body)

	return jsonByte
}

// JSONバイトデータの変換(入力が単一)
func convJsonByteToMap(jsonByte []byte) map[string]interface{} {
	// マップへ変換
	var data map[string]interface{}

	err := json.Unmarshal([]byte(jsonByte), &data)
	if err != nil {
		panic(err)
	}

	return data
}

// JSONバイトデータの変換(入力が複数)
func convJsonBytesToMaps(jsonByte []byte) []map[string]interface{} {
	str := string(jsonByte)
	parts := strings.Split(str, "\n")

	var allData []map[string]interface{}

	for _, part := range parts {
		part = strings.TrimSpace(part)
		if part == "" {
			continue
		}

		var data []map[string]interface{}
		err := json.Unmarshal([]byte(part), &data)
		if err != nil {
			panic(err)
		}

		allData = append(allData, data...)
	}

	return allData
}

/* GraphQL問い合わせ用 */

// JWTトークンの取得
// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/generating-a-json-web-token-jwt-for-a-github-app
func generateJWT() (string, error) {
	privateKey, err := os.ReadFile(GitHubAppPrivateKeyFile)
	if err != nil {
		return "", err
	}

	key, err := jwt.ParseRSAPrivateKeyFromPEM(privateKey)
	if err != nil {
		return "", err
	}

	// Create a new token object
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
		"iat": time.Now().Unix(),
		"exp": time.Now().Add(10 * time.Minute).Unix(),
		"iss": GitHubAppID,
	})

	// Sign and get the complete encoded token as a string using the secret
	tokenString, err := token.SignedString(key)

	return tokenString, err
}

// インストールアクセストークンURLの取得
// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
func getGitHubInstallAccessTokenURL(jwt string) (string, error) {
	// Use the JWT to authenticate with the GitHub API
	// https://docs.github.com/ja/rest/apps/apps?apiVersion=2022-11-28
	req, _ := http.NewRequest("GET", "https://api.github.com/app/installations", nil)
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("Authorization", "Bearer "+jwt)
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")

	// 結果の取得
	jsonByte := doHttp(req)
	data := convJsonBytesToMaps(jsonByte)

	return data[0]["access_tokens_url"].(string), nil
}

// インストールアクセストークンの取得
//
// (戻り値の例)
// "token": "ghs_xxx"
// "expires_at": "2023-10-31T06:32:37Z"
//
//	"permissions":
//	  "data":
//	    "contents": "read",
//	    "metadata": "read",
//	    "statuses": "read"
//
// "repository_selection": "all"
//
// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
func getGitHubInstallAccessToken(jwt string) (map[string]interface{}, error) {
	accessTokenUrl, err := getGitHubInstallAccessTokenURL(jwt)
	if err != nil {
		panic(err)
	}

	// Use the JWT to authenticate with the GitHub API
	// https://docs.github.com/ja/rest/apps/apps?apiVersion=2022-11-28
	req, _ := http.NewRequest("POST", accessTokenUrl, nil)
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("Authorization", "Bearer "+jwt)
	req.Header.Set("X-GitHub-Api-Version", "2022-11-28")

	// 結果の取得
	jsonByte := doHttp(req)
	data := convJsonByteToMap(jsonByte)

	return data, nil
}

// GitHub GraphQL APIの実行
// 引数 query クエリ文字列。改行・タブは自動削除されます。
// Explorer: https://docs.github.com/ja/graphql/overview/explorer
func RunGitHubGQLAPI(query string) (interface{}, error) {
	// MEMO: API呼び出しの度にJWTとアクセストークンを再生成している。
	// 頻繁にAPIをコールする場合は、処理が無駄なので個別管理する。
	jwt, err := generateJWT()
	if err != nil {
		panic(err)
	}

	accessToken, err := getGitHubInstallAccessToken(jwt)
	if err != nil {
		panic(err)
	}

	// queryから改行・タブを削除する。
	queryWithoutNewlines := strings.ReplaceAll(query, "\n", "")
	queryWithoutNewlinesAndTabs := strings.ReplaceAll(queryWithoutNewlines, "\t", "")

	// Use the AccessTokent to authenticate with the GitHub GraphQL API
	// https://docs.github.com/ja/apps/creating-github-apps/authenticating-with-a-github-app/authenticating-as-a-github-app-installation
	req, _ := http.NewRequest("POST", "https://api.github.com/graphql", strings.NewReader(queryWithoutNewlinesAndTabs))
	req.Header.Set("Accept", "application/vnd.github+json")
	req.Header.Set("Authorization", "Bearer "+accessToken["token"].(string))

	// 結果の取得
	jsonByte := doHttp(req)
	var data interface{}
	if len(jsonByte) > 0 && jsonByte[0] == '[' {
		// データを正常に取得できた場合
		data = convJsonBytesToMaps(jsonByte)
	} else {
		// エラーの場合(返却値が配列ではない)
		data = convJsonByteToMap(jsonByte)
	}

	return data, nil
}

使用go调用GraphQL的示例

我将调用先前的github.go文件中的RunGitHubGQLAPI函数。

import (
	"fmt"
	"log"
 )
 
func main() {
	// GraphQL query
	query := `{
		"query": "query {
			viewer {
			  repositories(first: 10) {
				nodes {
				  name
				  description
				  url
				}
				pageInfo {
				  endCursor
				  hasNextPage
				}
			  }
			}
		  }
		"
	}`

	data, err := RunGitHubGQLAPI(query)
	if err != nil {
		log.Fatalf("Error fetching GitHub repos: %v", err)
	}
    
    fmt.Println(data)
}

总结

我曾经以为调用GraphQL API会更简单,但实际上步骤很繁琐,非常麻烦。

尤其是在进行身份验证实现时,如果出现问题,几乎没有输出有用于错误调查的信息,大多数只会显示“凭证问题”,只能逐一检查每个步骤,这一点相当令人苦恼。

我对学习了一天的golang的感受

    • main関数があるファイルのpackageはmainではないとだめ。

 

    • 外部ファイルから呼び出される関数の名前の先頭は大文字。小文字だと内部のみになる。

 

    • ポインタがある。C/C++以外で初めてみた。パフォーマンスをカリカリに詰めるため?

 

    • throw/catchがなさそう。昔ながらの戻り値でエラーハンドリングが主流?

 

    • やたらinterfaceが出てくる。map[string]interface{}など、まだ馴染めない。

 

    • := は初回代入。= は2回目以降の代入。(Delphiでは := で初回、2回目以降の代入どちらも)

 

    • requireの参照元GitHubリポジトリが更新されたら、go get で最新コミットを取得しないといけない。

 

    関数の引数にデフォルト引数がない。なぜ・・・。

因为 Golang 在周围的评价很好,所以我第一次尝试使用了它,我的印象是它确实是一种轻量/紧凑的语言,就像大家说的那样。

虽然它很轻量/紧凑,但可以实现考虑到性能的处理,并且也有丰富的库,所以对于编写后端处理等想要快速完成的任务很方便。

不过,如果要使用 Golang 进行大规模编码,我不知道如何确保控制一致性。

请查阅文献库。

    • GitHub公式

 

    • ChatGPT先生

 

    その他、インターネット上のたくさんの記事

最终 (zuì

感谢您阅读到最后。
我们,Kakukaku株式会社,正在寻找与我们一起开发的员工和合作伙伴。
https://kakukaku.app/

广告
将在 10 秒后关闭
bannerAds