用Go简单实现Github授权登录并获取github用户信息

参考: 没错,用三方 Github 做授权登录就是这么简单!(OAuth2.0实战)-腾讯云开发者社区-腾讯云 (tencent.com)

首先我们需要了解一下什么是Oauth2.0

可以看阮一峰老师的这个文章::理解OAuth2.0

一口气说出 OAuth2.0 的四种授权方式 (qq.com)

一.授权流程

img

二.注册应用

要想得到一个网站的OAuth授权,必须要到它的网站进行身份注册,拿到应用的身份识别码 ClientIDClientSecret

注册 传送门 https://github.com/settings/applications/new,有几个必填项。

image-20240916144037542

提交后会看到就可以看到客户端ClientID 和客户端密匙ClientSecret,到这我们的准备工作就完事了。

image-20240916144119719

image-20240916153405142

三.授权开发

获取授权码

我们请求https://github.com/login/oauth/authorize?client_id=yourclient_id& redirect_uri=your_redirect_url/authorize

请求后会提示让我们授权,同意授权后会重定向到authorize/redirect,并携带授权码code;如果之前已经同意过,会跳过这一步直接回调。

image-20240916144502604

然后我就回跳转到redirect_url,能拿到这个code

http://localhost:8080/callback?code=ac3d9c25eb39752b34c2

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
)
const (
	clientID     = ""
	clientSecret = ""
	redirectURI  = "http://localhost:8080/callback" // 确保与 GitHub 应用配置一致
)
func loginHandler(w http.ResponseWriter, r *http.Request) {
	authURL := "https://github.com/login/oauth/authorize"
	u, err := url.Parse(authURL)
	if err != nil {
		http.Error(w, "Failed to build URL", http.StatusInternalServerError)
		return
	}
	q := u.Query()
	q.Set("client_id", clientID)
	q.Set("redirect_uri", redirectURI)
	u.RawQuery = q.Encode()
	http.Redirect(w, r, u.String(), http.StatusFound)
}
func main() {
	http.HandleFunc("/login", loginHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

既然我们知道了原理,就可以这么写,这样我们前端访问对应的api http://localhost:8080/login就可以直接重定向到http://localhost:8080/callback?code=ac3d9c25eb39752b34c2这个页面了

获取令牌

想获取access_token,我们需要向这个url发送post请求,携带client_id,client_secretcode参数

access_token 会作为请求响应返回,结果是个串字符。

根据上面的请求,我们就能写出对应的go代码

func exchangeCodeForToken(code string) (string, error) {
	tokenURL := "https://github.com/login/oauth/access_token"
	resp, err := http.PostForm(tokenURL, url.Values{
		"client_id":     {clientID},
		"client_secret": {clientSecret},
		"code":          {code},
	})
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get access token, status code: %d", resp.StatusCode)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	// Print the raw response for debugging purposes
	fmt.Printf("Token exchange response: %s\n", string(body))

	// Parse the response to extract the access token
	values, err := url.ParseQuery(string(body))
	if err != nil {
		return "", err
	}
	accessToken := values.Get("access_token")
	if accessToken == "" {
		return "", fmt.Errorf("access_token not found in response")
	}
	return accessToken, nil
}

可以拿到下面的打印:

Token exchange response: access_token=gho_pSgXPcPLKPuMBIcse&scope=read%3Auser%2Cuser%3Aemail&token_type=bearer

以上access_token已脱敏

我们再代码中对body进行解析,并将其转换为 map[string][]string 类型的键值对,然后单独取出access_token字段

有了令牌以后开始获取用户信息,在 API 中要带上access_token

获取用户信息

有了令牌以后开始获取用户信息,在 API 中要带上access_token

func getUserInfo(token string) (string, error) {
	userURL := "https://api.github.com/user"
	req, err := http.NewRequest("GET", userURL, nil)
	if err != nil {
		return "", err
	}
	req.Header.Add("Authorization", "Bearer "+token)
	req.Header.Add("User-Agent", "Go OAuth App") // GitHub API requires a User-Agent header
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return string(body), nil
}

只需要再请求头里面带上Authorization: Bearer token,设置一个User-Agent就可以了


下面附上完整代码:

package main

import (
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
	"net/url"
)

const (
	clientID     = ""
	clientSecret = ""
	redirectURI  = "http://localhost:8080/callback" // 确保与 GitHub 应用配置一致
)

func main() {
	http.HandleFunc("/login", loginHandler)
	http.HandleFunc("/callback", callbackHandler)
	log.Fatal(http.ListenAndServe(":8080", nil))
}

func loginHandler(w http.ResponseWriter, r *http.Request) {
	authURL := "https://github.com/login/oauth/authorize"
	u, err := url.Parse(authURL)
	if err != nil {
		http.Error(w, "Failed to build URL", http.StatusInternalServerError)
		return
	}
	q := u.Query()
	q.Set("client_id", clientID)
	q.Set("redirect_uri", redirectURI)
	u.RawQuery = q.Encode()
	http.Redirect(w, r, u.String(), http.StatusFound)
}

func callbackHandler(w http.ResponseWriter, r *http.Request) {
	code := r.URL.Query().Get("code")
	fmt.Println("Authorization code:", code)
	token, err := exchangeCodeForToken(code)
	// fmt.Println(token)
	if err != nil {
		http.Error(w, "Failed to get access token", http.StatusInternalServerError)
		fmt.Println("Error getting access token:", err)
		return
	}
	userInfo, err := getUserInfo(token)
	if err != nil {
		http.Error(w, "Failed to get user info", http.StatusInternalServerError)
		fmt.Println("Error getting user info:", err)
		return
	}
	fmt.Fprintf(w, "User Info: %s", userInfo)
	fmt.Printf("UserInfo: %s", userInfo)
}

func exchangeCodeForToken(code string) (string, error) {
	tokenURL := "https://github.com/login/oauth/access_token"
	resp, err := http.PostForm(tokenURL, url.Values{
		"client_id":     {clientID},
		"client_secret": {clientSecret},
		"code":          {code},
		"redirect_uri":  {redirectURI},
	})
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get access token, status code: %d", resp.StatusCode)
	}

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	// Print the raw response for debugging purposes
	fmt.Printf("Token exchange response: %s\n", string(body))

	// Parse the response to extract the access token
	values, err := url.ParseQuery(string(body))
	if err != nil {
		return "", err
	}
	accessToken := values.Get("access_token")
	if accessToken == "" {
		return "", fmt.Errorf("access_token not found in response")
	}
	return accessToken, nil
}

func getUserInfo(token string) (string, error) {
	userURL := "https://api.github.com/user"
	req, err := http.NewRequest("GET", userURL, nil)
	if err != nil {
		return "", err
	}
	req.Header.Add("Authorization", "Bearer "+token)
	req.Header.Add("User-Agent", "Go OAuth App") // GitHub API requires a User-Agent header
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close()

	body, err := ioutil.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}
	return string(body), nil
}

可以拿到下面的响应:

UserInfo: {"login":"meowrain","id":107172084,"node_id":"U_kgDOBmNQ9A","avatar_url":"https://avatars.githubusercontent.com/u/107172084?v=4","gravatar_id":"","url":"https://api.github.com/users/meowrain","html_url":"https://github.com/meowrain","followers_url":"https://api.github.com/users/meowrain/followers","following_url":"https://api.github.com/users/meowrain/following{/other_user}","gists_url":"https://api.github.com/users/meowrain/gists{/gist_id}","starred_url":"https://api.github.com/users/meowrain/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/meowrain/subscriptions","organizations_url":"https://api.github.com/users/meowrain/orgs","repos_url":"https://api.github.com/users/meowrain/repos","events_url":"https://api.github.com/users/meowrain/events{/privacy}","received_events_url":"https://api.github.com/users/meowrain/received_events","type":"User","site_admin":false,"name":"MeowRain","company":"Shanxi Agricultural University","blog":"https://meowrain.cn","location":"China","email":"meowrain@126.com","hireable":null,"bio":"Gopher,Ciallo~(∠・ω< )⌒★","twitter_username":null,"notification_email":"meowrain@126.com","public_repos":76,"public_gists":1,"followers":38,"following":123,"created_at":"2022-06-09T06:33:13Z","updated_at":"2024-09-11T15:02:43Z","private_gists":1,"total_private_repos":8,"owned_private_repos":6,"disk_usage":1716464,"collaborators":0,"two_factor_authentication":true,"plan":{"name":"pro","space":976562499,"collaborators":0,"private_repos":9999}}

image-20240916150817759

我们使用json格式化工具格式化一下看看返回了什么信息

{
  "login": "meowrain",
  "id": 107172084,
  "node_id": "U_kgDOBmNQ9A",
  "avatar_url": "https://avatars.githubusercontent.com/u/107172084?v=4",
  "gravatar_id": "",
  "url": "https://api.github.com/users/meowrain",
  "html_url": "https://github.com/meowrain",
  "followers_url": "https://api.github.com/users/meowrain/followers",
  "following_url": "https://api.github.com/users/meowrain/following{/other_user}",
  "gists_url": "https://api.github.com/users/meowrain/gists{/gist_id}",
  "starred_url": "https://api.github.com/users/meowrain/starred{/owner}{/repo}",
  "subscriptions_url": "https://api.github.com/users/meowrain/subscriptions",
  "organizations_url": "https://api.github.com/users/meowrain/orgs",
  "repos_url": "https://api.github.com/users/meowrain/repos",
  "events_url": "https://api.github.com/users/meowrain/events{/privacy}",
  "received_events_url": "https://api.github.com/users/meowrain/received_events",
  "type": "User",
  "site_admin": false,
  "name": "MeowRain",
  "company": "Shanxi Agricultural University",
  "blog": "https://meowrain.cn",
  "location": "China",
  "email": "meowrain@126.com",
  "hireable": null,
  "bio": "Gopher,Ciallo~(∠・ω< )⌒★",
  "twitter_username": null,
  "notification_email": "meowrain@126.com",
  "public_repos": 76,
  "public_gists": 1,
  "followers": 38,
  "following": 123,
  "created_at": "2022-06-09T06:33:13Z",
  "updated_at": "2024-09-11T15:02:43Z",
  "private_gists": 1,
  "total_private_repos": 8,
  "owned_private_repos": 6,
  "disk_usage": 1716464,
  "collaborators": 0,
  "two_factor_authentication": true,
  "plan": {
    "name": "pro",
    "space": 976562499,
    "collaborators": 0,
    "private_repos": 9999
  }
}

四.封装到应用程序

package open_login

import (
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"net/url"
)

const (
	tokenURL  = "https://github.com/login/oauth/access_token"
	userURL   = "https://api.github.com/user"
	userAgent = "Go OAuth App" // GitHub API requires a User-Agent header
)

type GithubInfo struct {
	Name        string `json:"name"`       //昵称
	Avatar      string `json:"avatar_url"` //头像
	AccessToken string `json:"accesstoken"`
}
type GithubLogin struct {
	clientID     string
	clientSecret string
	redirectURI  string
	code         string
	AccessToken  string
}
type GithubConfig struct {
	ClientID     string
	ClientSecret string
	RedirectURI  string
}

func NewGithubLogin(code string, conf GithubConfig) (githubInfo GithubInfo, err error) {
	githubLogin := &GithubLogin{
		clientID:     conf.ClientID,
		clientSecret: conf.ClientSecret,
		redirectURI:  conf.RedirectURI,
		code:         code,
	}
	err = githubLogin.GetAccessToken()
	if err != nil {
		return githubInfo, err
	}
	githubInfo, err = githubLogin.GetUserInfo()
	if err != nil {
		return githubInfo, err
	}
	githubInfo.AccessToken = githubLogin.AccessToken
	return githubInfo, nil

}
func GetUserInfo(accessToken string) (githubInfo GithubInfo,err error) {
    req, err := http.NewRequest("GET", userURL, nil)
	if err != nil {
		return GithubInfo{}, err
	}
	req.Header.Add("Authorization", "Bearer "+accessToken)
	req.Header.Add("User-Agent", userAgent) // GitHub API requires a User-Agent header
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return GithubInfo{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return GithubInfo{}, fmt.Errorf("failed to get user info, status code: %d", resp.StatusCode)
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return GithubInfo{}, err
	}
	var githubInfo GithubInfo
	if err := json.Unmarshal(body, &githubInfo); err != nil {
		return GithubInfo{}, err
	}
	return githubInfo, nil
}
// GetAccessToken exchanges the authorization code for an access token
func (g *GithubLogin) GetAccessToken() error {
	resp, err := http.PostForm(tokenURL, url.Values{
		"client_id":     {g.clientID},
		"client_secret": {g.clientSecret},
		"code":          {g.code},
		"redirect_uri":  {g.redirectURI},
	})
	if err != nil {
		return err
	}
	defer resp.Body.Close()

	if resp.StatusCode != http.StatusOK {
		return fmt.Errorf("failed to get access token, status code: %d", resp.StatusCode)
	}

	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return err
	}

	// Parse the response to extract the access token
	values, err := url.ParseQuery(string(body))
	if err != nil {
		return err
	}
	g.AccessToken = values.Get("access_token")
	if g.AccessToken == "" {
		return fmt.Errorf("access_token not found in response")
	}
	return nil
}
func (g *GithubLogin) GetUserInfo() (GithubInfo, error) {
	req, err := http.NewRequest("GET", userURL, nil)
	if err != nil {
		return GithubInfo{}, err
	}
	req.Header.Add("Authorization", "Bearer "+g.AccessToken)
	req.Header.Add("User-Agent", userAgent) // GitHub API requires a User-Agent header
	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		return GithubInfo{}, err
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusOK {
		return GithubInfo{}, fmt.Errorf("failed to get user info, status code: %d", resp.StatusCode)
	}
	body, err := io.ReadAll(resp.Body)
	if err != nil {
		return GithubInfo{}, err
	}
	var githubInfo GithubInfo
	if err := json.Unmarshal(body, &githubInfo); err != nil {
		return GithubInfo{}, err
	}
	return githubInfo, nil
}

我们写注册登录逻辑就能这么写了

	case "github":
		info, err := open_login.NewGithubLogin(req.Code, open_login.GithubConfig{
			ClientID:     l.svcCtx.Config.Github.ClientID,
			ClientSecret: l.svcCtx.Config.Github.ClientSecret,
			RedirectURI:  l.svcCtx.Config.Github.RedirectURI,
		})
		if err != nil {
			logx.Error(err)
			return nil, errors.New("登录失败")
		}
		var user auth_models.UserModel
		err = l.svcCtx.DB.Take(&user, "open_id = ?", info.AccessToken).Error
		if err != nil {
			//注册逻辑,存储或者更新access_token和用户信息到数据库用户表中
		}
		//登录逻辑