用Go简单实现Github授权登录并获取github用户信息
参考: 没错,用三方 Github 做授权登录就是这么简单!(OAuth2.0实战)-腾讯云开发者社区-腾讯云 (tencent.com)
首先我们需要了解一下什么是Oauth2.0
可以看阮一峰老师的这个文章::理解OAuth2.0
一.授权流程
二.注册应用
要想得到一个网站的OAuth
授权,必须要到它的网站进行身份注册,拿到应用的身份识别码 ClientID
和 ClientSecret
。
注册 传送门 https://github.com/settings/applications/new
,有几个必填项。
-
Application name
:我们的应用名; -
Homepage URL
:应用主页链接; -
Authorization callback URL
:这个是github
回调我们项目的地址,用来获取授权码和令牌。https://docs.github.com/zh/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps
提交后会看到就可以看到客户端ClientID
和客户端密匙ClientSecret
,到这我们的准备工作就完事了。
三.授权开发
获取授权码
我们请求https://github.com/login/oauth/authorize?client_id=yourclient_id& redirect_uri=your_redirect_url/authorize
请求后会提示让我们授权,同意授权后会重定向到authorize/redirect
,并携带授权码code
;如果之前已经同意过,会跳过这一步直接回调。
然后我就回跳转到redirect_url,能拿到这个code
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_secret
,code
参数
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}}
我们使用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和用户信息到数据库用户表中
}
//登录逻辑