Golang 前後端分離項目 OAuth2 教程
什麼是 OAuth2
OAuth2.0 是 OAuth 協議的下一版本, 但不向後兼容 OAuth 1.0 即完全廢止了 OAuth1.0. OAuth 2.0 關注客戶端開發者的簡易性. 要麼通過組織在資源擁有者和 HTTP 服務商之間的被批准的交互動作代表用戶, 要麼允許第三方應用代表用戶獲得訪問的權限. 同時爲 Web 應用, 桌面應用和手機, 和起居室設備提供專門的認證流程. 2012 年 10 月, OAuth 2.0 協議正式發佈爲 RFC 6749. 在認證和授權的過程中涉及的三方包括:
-
服務提供方, 用戶使用服務提供方來存儲受保護的資源, 如照片, 視頻, 聯繫人列表.
-
用戶, 存放在服務提供方的受保護的資源的擁有者.
-
客戶端, 要訪問服務提供方資源的第三方應用, 通常是網站, 如提供照片打印服務的網站. 在認證過程之前, 客戶端要向服務提供者申請客戶端標識.
使用 OAuth 進行認證和授權的過程如下所示:
-
(A)用戶打開客戶端以後, 客戶端要求用戶給予授權.
-
(B)用戶同意給予客戶端授權.
-
(C)客戶端使用上一步獲得的授權, 向認證服務器申請令牌.
-
(D)認證服務器對客戶端進行認證以後, 確認無誤, 同意發放令牌.
-
(E)客戶端使用令牌, 向資源服務器申請獲取資源.
-
(F)資源服務器確認令牌無誤, 同意向客戶端開放資源
GitHub OAuth2 第三方登錄示例教程
1. 應用註冊
一個應用要求 OAuth 授權, 必須先到對方網站登記, 讓對方知道是誰在請求.
所以, 您要先去 GitHub 登記一下. 當然, 我已經登記過了, 您使用我的登記信息也可以, 但爲了完整走一遍流程, 還是建議大家自己登記. 這是免費的. OAuth 應用註冊地址 https://github.com/settings/applications/new, 或者登陸自己的 GitHub 賬號, settings -> Developer Settings -> OAuth Apps -> New OAuth App
填寫自己的callback
地址
2. 獲取ClientId
, ClientSecret
和 callback地址
這裏我使用viper
爲golang
管理配置文件, 因爲是前後端分離項目所以比傳統的網站複雜一點點.
配置文件 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_id
和github.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
需要提供三個參數.
-
$client_id$
:客戶端的 ID -
client_secret
:客戶端的密鑰 -
code
:授權碼 作爲迴應, GitHub 會返回一段 JSON 數據, 裏面包含了令牌accessToken
.
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