Golang 前後端分離項目 OAuth2 教程

什麼是 OAuth2

OAuth2.0 是 OAuth 協議的下一版本, 但不向後兼容 OAuth 1.0 即完全廢止了 OAuth1.0. OAuth 2.0 關注客戶端開發者的簡易性. 要麼通過組織在資源擁有者和 HTTP 服務商之間的被批准的交互動作代表用戶, 要麼允許第三方應用代表用戶獲得訪問的權限. 同時爲 Web 應用, 桌面應用和手機, 和起居室設備提供專門的認證流程. 2012 年 10 月, OAuth 2.0 協議正式發佈爲 RFC 6749. 在認證和授權的過程中涉及的三方包括:

  1. 服務提供方, 用戶使用服務提供方來存儲受保護的資源, 如照片, 視頻, 聯繫人列表.

  2. 用戶, 存放在服務提供方的受保護的資源的擁有者.

  3. 客戶端, 要訪問服務提供方資源的第三方應用, 通常是網站, 如提供照片打印服務的網站. 在認證過程之前, 客戶端要向服務提供者申請客戶端標識.

使用 OAuth 進行認證和授權的過程如下所示:

GitHub OAuth2 第三方登錄示例教程

1. 應用註冊

一個應用要求 OAuth 授權, 必須先到對方網站登記, 讓對方知道是誰在請求.

所以, 您要先去 GitHub 登記一下. 當然, 我已經登記過了, 您使用我的登記信息也可以, 但爲了完整走一遍流程, 還是建議大家自己登記. 這是免費的. OAuth 應用註冊地址 https://github.com/settings/applications/new, 或者登陸自己的 GitHub 賬號, settings -> Developer Settings -> OAuth Apps -> New OAuth App

填寫自己的callback地址

2. 獲取ClientIdClientSecret 和 callback地址

這裏我使用vipergolang管理配置文件, 因爲是前後端分離項目所以比傳統的網站複雜一點點.

配置文件 config.toml

[github]
    client_id="xxxID"
    client_secret="xxxxxx"
    callback_url="http://localhost:8080/#/" # vuejs 頁面來處理callback

加載配置文件代碼, 詳細 Go 進階: 怎麼使用 viper 管理配置

func initViperConfigFile(configFile string) {
    viper.SetConfigName(configFile)        // name of config file (without extension)
    viper.AddConfigPath("/etc/fortress/")  // path to look for the config file in
    viper.AddConfigPath("$HOME/.fortress") // call multiple times to add many search paths
    viper.AddConfigPath(".")               // optionally look for config in the working directory
    err := viper.ReadInConfig()            // Find and read the config file
    if err != nil {                        // Handle errors reading the config file
        log.Fatal("application configuration'initialization is failed", err)
    }
}

3. 前端 (Vuejs SPA) 處理登陸按鈕跳轉到 Github 授權頁面

前端頁面登陸按鈕跳轉 html<el-button type="warning" @click.native.prevent="handleLoginGithub" v-text="Github第三方賬號登陸"> </el-button> 後端提供接口傳送配置文件中的github.client_idgithub.client_url地址給前端, 前端按鈕點擊 跳轉到 github 授權頁面

//跳轉到GitHub授權頁面
handleLoginGithub() {
    const url = `https://github.com/login/oauth/authorize?client_id=${this.github_client_id}&scope=user:email&allow_signup=true`;
    window.location.href = url
},

client_id是必須參數 callback 參數空瀏覽器則跳轉到您 GitHub OAuth APP 配置的頁面 (這個頁面將會帶上一個 code 參數) 更詳細參數見 github.com OAuth 文檔

4. 授權碼code

登錄後, GitHub 詢問用戶, 該應用正在請求數據, 您是否同意授權.

用戶同意授權, GitHub 就會跳轉到 redirect_uri 指定的跳轉網址, 並且帶上授權碼, 跳轉回來的 URL 就是下面的樣子. http://localhost:8080/?code=4f307b926cc11ae4883b#/login. 其中 code 用來換取 github.com token

5. 前端 Vuejs(SPA) 項目得到 code 參數發送給 golang 後端服務

注意: 我的 vuejs SPA 使用的 vue-router Hash 模式 github.com 配置的 callback 是前端登陸頁面, mounted 方法檢測 是否帶有參數code 如果 query 參數部位空把 code通過fetchGithubUserLoginByCodeOrDoNothing 發送給後端

    export default {
        data() {
            return {
                github_client_id: "",
            };
        },
        computed: {
            //計算屬性獲取,url中query code 參數
            code() {
                const urlParams = new URLSearchParams(window.location.search);
                const myParam = urlParams.get('code');
                return myParam || false;
            },
        },
        mounted() {
            //獲取配置cong golang 後端
            this.$http.get('meta').then(res ={
                if (res) {
                    this.github_client_id = res.data.github_client_id;
                    this.github_callback_url = res.data.github_callback_url;
                    //使用github Oauth登陸
                    this.fetchGithubUserLoginByCodeOrDoNothing()
                }
            });
        },
        methods: {
            //code 發送給後端
            fetchGithubUserLoginByCodeOrDoNothing() {
                //使用github Oauth登陸
                if (this.code) {
                    let data = {code: this.code};
                    this.$http.get('login-github'{params: data}).then(res ={
                        if (res) {
                            localStorage.setItem("token", res.data.token);
                            localStorage.setItem("expire_ts", res.data.expire_ts);
                            localStorage.setItem("expire", res.data.expire);
                            this.$store.commit('setUser', res.data);
                            this.$router.push({name: "machine"});
                        }
                    })
                }
            },
            //跳轉到GitHub 授權頁面
            handleLoginGithub() {
                const url = `https://github.com/login/oauth/authorize?client_id=${this.github_client_id}&scope=user:email&allow_signup=true`;
                window.location.href = url
            },
        }
    }

6. Golang 後端收到 github.com OAuth code

1. 通過 code 調用 POST https://github.com/login/oauth/access_token 獲取 token 參數

golang Gin 路由配置: r.GET("api/login-github", handler.LoginGithub)

//LoginGithub github OAuth 登陸
func LoginGithub(c *gin.Context) {
    //獲取github.com OAuth APP給的token
    code := c.Query("code")
    //code 通過 github.com OAuth API 換取 token
    // token 根據GitHub 開發API接口獲取用戶信息 githubUser
    gu, err := fetchGithubUser(code)
    if err != nil {
        jsonError(c, err)
        return
    }
....
....
....

2. 令牌通過 token 調通 GET https://api.github.com/user 獲取授權的用戶信息

代碼中, GitHub 的令牌接口https://github.com/login/oauth/access_token需要提供三個參數.

type githubToken struct {
    AccessToken string `json:"access_token"`
    Scope       string `json:"scope"`
    TokenType   string `json:"token_type"`
}

//fetchGithubUser 獲取github 用戶信息
func fetchGithubUser(code string) (*githubUser, error) {
    client := http.Client{}
    params := fmt.Sprintf(`{"client_id":"%s","client_secret":"%s","code":"%s"}`, viper.GetString("github.client_id"), viper.GetString("github.client_secret"), code)
    req, err := http.NewRequest("POST""https://github.com/login/oauth/access_token", bytes.NewBufferString(params))
    if err != nil {
        return nil, err
    }
    req.Header.Add("Accept""application/json")
    req.Header.Add("Content-type""application/json")
    res, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    defer res.Body.Close()
    bs, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }

    gt := githubToken{}
    err = json.Unmarshal(bs, >)
    if err != nil {
        return nil, err
    }
    //得到token struct
....
....
....
....

3. API 數據通過數據庫是否曾在用戶, 如果存在生成對應用戶在自己 APP 中的 JWT token, 如果沒有則創建用戶, 生成自己應用的 jwt token

GitHub API 的地址是https://api.github.com/user, 請求的時候必須在 HTTP 頭信息裏面帶上令牌 Authorization: Bearer 361507da. 然後, 就可以拿到用戶數據, 得到用戶的身份.

//fetchGithubUser 獲取github 用戶信息
func fetchGithubUser(code string) (*githubUser, error) {
    client := http.Client{}
...
...
...
    //開始獲取用戶信息
    req, err = http.NewRequest("GET""https://api.github.com/user", nil)
    req.Header.Add("Authorization""Bearer "+gt.AccessToken)

    res, err = client.Do(req)
    if err != nil {
        return nil, err
    }
    if res.StatusCode != 200 {
        return nil, errors.New("using github token to fetch User Info failed with not 200 error")
    }
    defer res.Body.Close()
    bs, err = ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    gu := &githubUser{}
    err = json.Unmarshal(bs, gu)
    if err != nil {
        return nil, err
    }
    if gu.Email == nil {
        tEmail := fmt.Sprintf("%d@github.com", gu.ID)
        gu.Email = &tEmail
    }
    gu.Token = gt.AccessToken
    return gu, nil
}

type githubUser struct {
    Login             string    `json:"login"`
    ID                int       `json:"id"`
    NodeID            string    `json:"node_id"`
    AvatarURL         string    `json:"avatar_url"`
    GravatarID        string    `json:"gravatar_id"`
    URL               string    `json:"url"`
    HTMLURL           string    `json:"html_url"`
    FollowersURL      string    `json:"followers_url"`
    FollowingURL      string    `json:"following_url"`
    GistsURL          string    `json:"gists_url"`
    StarredURL        string    `json:"starred_url"`
    SubscriptionsURL  string    `json:"subscriptions_url"`
    OrganizationsURL  string    `json:"organizations_url"`
    ReposURL          string    `json:"repos_url"`
    EventsURL         string    `json:"events_url"`
    ReceivedEventsURL string    `json:"received_events_url"`
    Type              string    `json:"type"`
    SiteAdmin         bool      `json:"site_admin"`
    Name              string    `json:"name"`
    Blog              string    `json:"blog"`
    Location          string    `json:"location"`
    Email             *string   `json:"email"`
    Hireable          bool      `json:"hireable"`
    Bio               string    `json:"bio"`
    PublicRepos       int       `json:"public_repos"`
    PublicGists       int       `json:"public_gists"`
    Followers         int       `json:"followers"`
    Following         int       `json:"following"`
    CreatedAt         time.Time `json:"created_at"`
    UpdatedAt         time.Time `json:"updated_at"`
    Token             string    `json:"-"`
}

h_login_github.go 完整代碼是這樣的

package handler

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "fortress/model"
    "github.com/gin-gonic/gin"
    "github.com/spf13/viper"
    "io/ioutil"
    "net/http"
    "time"
)



//LoginGithub github OAuth 登陸
func LoginGithub(c *gin.Context) {

    //獲取github.com OAuth APP給的token
    code := c.Query("code")
    //code 通過 github.com OAuth API 換取 token
    // token 根據GitHub 開發API接口獲取用戶信息 githubUser
    gu, err := fetchGithubUser(code)
    if err != nil {
        jsonError(c, err)
        return
    }

    user := model.User{}
    //比對或者插入GitHub User 到數據庫
    //同時參數自己的jwt token
    data, err := user.LoginGithub(*gu.Email, gu.Login, gu.Name, gu.Bio, gu.AvatarURL, gu.Token)
    if handleError(c, err) {
        return
    }
    jsonData(c, data)
}

//fetchGithubUser 獲取github 用戶信息
func fetchGithubUser(code string) (*githubUser, error) {
    client := http.Client{}
    params := fmt.Sprintf(`{"client_id":"%s","client_secret":"%s","code":"%s"}`, viper.GetString("github.client_id"), viper.GetString("github.client_secret"), code)
    req, err := http.NewRequest("POST""https://github.com/login/oauth/access_token", bytes.NewBufferString(params))
    if err != nil {
        return nil, err
    }
    req.Header.Add("Accept""application/json")
    req.Header.Add("Content-type""application/json")
    res, err := client.Do(req)
    if err != nil {
        return nil, err
    }

    defer res.Body.Close()
    bs, err := ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }

    gt := githubToken{}
    err = json.Unmarshal(bs, >)
    if err != nil {
        return nil, err
    }

    //開始獲取用戶信息
    req, err = http.NewRequest("GET""https://api.github.com/user", nil)
    req.Header.Add("Authorization""Bearer "+gt.AccessToken)

    res, err = client.Do(req)
    if err != nil {
        return nil, err
    }
    if res.StatusCode != 200 {
        return nil, errors.New("using github token to fetch User Info failed with not 200 error")
    }
    defer res.Body.Close()
    bs, err = ioutil.ReadAll(res.Body)
    if err != nil {
        return nil, err
    }
    gu := &githubUser{}
    err = json.Unmarshal(bs, gu)
    if err != nil {
        return nil, err
    }
    if gu.Email == nil {
        tEmail := fmt.Sprintf("%d@github.com", gu.ID)
        gu.Email = &tEmail
    }
    gu.Token = gt.AccessToken
    return gu, nil
}

type githubToken struct {
    AccessToken string `json:"access_token"`
    Scope       string `json:"scope"`
    TokenType   string `json:"token_type"`
}
type githubUser struct {
    Login             string    `json:"login"`
    ID                int       `json:"id"`
    NodeID            string    `json:"node_id"`
    AvatarURL         string    `json:"avatar_url"`
    GravatarID        string    `json:"gravatar_id"`
    URL               string    `json:"url"`
    HTMLURL           string    `json:"html_url"`
    FollowersURL      string    `json:"followers_url"`
    FollowingURL      string    `json:"following_url"`
    GistsURL          string    `json:"gists_url"`
    StarredURL        string    `json:"starred_url"`
    SubscriptionsURL  string    `json:"subscriptions_url"`
    OrganizationsURL  string    `json:"organizations_url"`
    ReposURL          string    `json:"repos_url"`
    EventsURL         string    `json:"events_url"`
    ReceivedEventsURL string    `json:"received_events_url"`
    Type              string    `json:"type"`
    SiteAdmin         bool      `json:"site_admin"`
    Name              string    `json:"name"`
    Blog              string    `json:"blog"`
    Location          string    `json:"location"`
    Email             *string   `json:"email"`
    Hireable          bool      `json:"hireable"`
    Bio               string    `json:"bio"`
    PublicRepos       int       `json:"public_repos"`
    PublicGists       int       `json:"public_gists"`
    Followers         int       `json:"followers"`
    Following         int       `json:"following"`
    CreatedAt         time.Time `json:"created_at"`
    UpdatedAt         time.Time `json:"updated_at"`
    Token             string    `json:"-"`
}v

轉自:

mojotv.cn/2019/08/05/golang-oauth2-login

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/ZBtlu58BenguCId_aoFkEw