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

用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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
}

可以拿到下面的打印:

1
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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
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就可以了


下面附上完整代码:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
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
}

可以拿到下面的响应:

1
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格式化工具格式化一下看看返回了什么信息

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
{
  "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
  }
}

四.封装到应用程序

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
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
}

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

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
	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和用户信息到数据库用户表中
		}
		//登录逻辑
		

相关内容

0%