OAuth如何構(gòu)建一個(gè)即時(shí)消息應(yīng)用

這篇“OAuth如何構(gòu)建一個(gè)即時(shí)消息應(yīng)用”文章的知識(shí)點(diǎn)大部分人都不太理解,所以小編給大家總結(jié)了以下內(nèi)容,內(nèi)容詳細(xì),步驟清晰,具有一定的借鑒價(jià)值,希望大家閱讀完這篇文章能有所收獲,下面我們一起來(lái)看看這篇“OAuth如何構(gòu)建一個(gè)即時(shí)消息應(yīng)用”文章吧。

成都創(chuàng)新互聯(lián)公司自2013年起,先為屏南等服務(wù)建站,屏南等地企業(yè),進(jìn)行企業(yè)商務(wù)咨詢服務(wù)。為屏南企業(yè)網(wǎng)站制作PC+手機(jī)+微官網(wǎng)三網(wǎng)同步一站式服務(wù)解決您的所有建站問(wèn)題。

社交登錄的工作方式十分簡(jiǎn)單:用戶點(diǎn)擊鏈接,然后重定向到 GitHub  授權(quán)頁(yè)面。當(dāng)用戶授予我們對(duì)他的個(gè)人信息的訪問(wèn)權(quán)限之后,就會(huì)重定向回登錄頁(yè)面。下一次嘗試登錄時(shí),系統(tǒng)將不會(huì)再次請(qǐng)求授權(quán),也就是說(shuō),我們的應(yīng)用已經(jīng)記住了這個(gè)用戶。這使得整個(gè)登錄流程看起來(lái)就和你用鼠標(biāo)單擊一樣快。

如果進(jìn)一步考慮其內(nèi)部實(shí)現(xiàn)的話,過(guò)程就會(huì)變得復(fù)雜起來(lái)。首先,我們需要注冊(cè)一個(gè)新的 GitHub OAuth 應(yīng)用。

這一步中,比較重要的是回調(diào) URL。我們將它設(shè)置為 http://localhost:3000/api/oauth/github/callback。這是因?yàn)?,在開(kāi)發(fā)過(guò)程中,我們總是在本地主機(jī)上工作。一旦你要將應(yīng)用交付生產(chǎn),請(qǐng)使用正確的回調(diào) URL 注冊(cè)一個(gè)新的應(yīng)用。

注冊(cè)以后,你將會(huì)收到“客戶端 id”和“安全密鑰”。安全起見(jiàn),請(qǐng)不要與任何人分享他們 ?

順便讓我們開(kāi)始寫(xiě)一些代碼吧。現(xiàn)在,創(chuàng)建一個(gè) main.go 文件:

package main import (    "database/sql"    "fmt"    "log"    "net/http"    "net/url"    "os"    "strconv"     "github.com/gorilla/securecookie"    "github.com/joho/godotenv"    "github.com/knq/jwt"    _ "github.com/lib/pq"    "github.com/matryer/way"    "golang.org/x/oauth3"    "golang.org/x/oauth3/github") var origin *url.URLvar db *sql.DBvar githubOAuthConfig *oauth3.Configvar cookieSigner *securecookie.SecureCookievar jwtSigner jwt.Signer func main() {    godotenv.Load()     port := intEnv("PORT", 3000)    originString := env("ORIGIN", fmt.Sprintf("http://localhost:%d/", port))    databaseURL := env("DATABASE_URL", "postgresql://root@127.0.0.1:26257/messenger?sslmode=disable")    githubClientID := os.Getenv("GITHUB_CLIENT_ID")    githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")    hashKey := env("HASH_KEY", "secret")    jwtKey := env("JWT_KEY", "secret")     var err error    if origin, err = url.Parse(originString); err != nil || !origin.IsAbs() {        log.Fatal("invalid origin")        return    }     if i, err := strconv.Atoi(origin.Port()); err == nil {        port = i    }     if githubClientID == "" || githubClientSecret == "" {        log.Fatalf("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET")        return    }     if db, err = sql.Open("postgres", databaseURL); err != nil {        log.Fatalf("could not open database connection: %v\n", err)        return    }    defer db.Close()    if err = db.Ping(); err != nil {        log.Fatalf("could not ping to db: %v\n", err)        return    }     githubRedirectURL := *origin    githubRedirectURL.Path = "/api/oauth/github/callback"    githubOAuthConfig = &oauth3.Config{        ClientID:     githubClientID,        ClientSecret: githubClientSecret,        Endpoint:     github.Endpoint,        RedirectURL:  githubRedirectURL.String(),        Scopes:       []string{"read:user"},    }     cookieSigner = securecookie.New([]byte(hashKey), nil).MaxAge(0)     jwtSigner, err = jwt.HS256.New([]byte(jwtKey))    if err != nil {        log.Fatalf("could not create JWT signer: %v\n", err)        return    }     router := way.NewRouter()    router.HandleFunc("GET", "/api/oauth/github", githubOAuthStart)    router.HandleFunc("GET", "/api/oauth/github/callback", githubOAuthCallback)    router.HandleFunc("GET", "/api/auth_user", guard(getAuthUser))     log.Printf("accepting connections on port %d\n", port)    log.Printf("starting server at %s\n", origin.String())    addr := fmt.Sprintf(":%d", port)    if err = http.ListenAndServe(addr, router); err != nil {        log.Fatalf("could not start server: %v\n", err)    }} func env(key, fallbackValue string) string {    v, ok := os.LookupEnv(key)    if !ok {        return fallbackValue    }    return v} func intEnv(key string, fallbackValue int) int {    v, ok := os.LookupEnv(key)    if !ok {        return fallbackValue    }    i, err := strconv.Atoi(v)    if err != nil {        return fallbackValue    }    return i}

安裝依賴項(xiàng):

go get -u github.com/gorilla/securecookiego get -u github.com/joho/godotenvgo get -u github.com/knq/jwtgo get -u github.com/lib/pqge get -u github.com/matoous/go-nanoidgo get -u github.com/matryer/waygo get -u golang.org/x/oauth3

我們將會(huì)使用 .env 文件來(lái)保存密鑰和其他配置。請(qǐng)創(chuàng)建這個(gè)文件,并保證里面至少包含以下內(nèi)容:

GITHUB_CLIENT_ID=your_github_client_idGITHUB_CLIENT_SECRET=your_github_client_secret

我們還要用到的其他環(huán)境變量有:

  • PORT服務(wù)器運(yùn)行的端口,默認(rèn)值是 3000。

  • ORIGIN:你的域名,默認(rèn)值是 http://localhost:3000/。我們也可以在這里指定端口。

  • DATABASE_URL:Cockroach 數(shù)據(jù)庫(kù)的地址。默認(rèn)值是 postgresql://root@127.0.0.1:26257/messenger?sslmode=disable。

  • HASH_KEY:用于為 cookie 簽名的密鑰。沒(méi)錯(cuò),我們會(huì)使用已簽名的 cookie 來(lái)確保安全。

  • JWT_KEY:用于簽署 JSON

    網(wǎng)絡(luò)令牌Web Token

    的密鑰。

因?yàn)榇a中已經(jīng)設(shè)定了默認(rèn)值,所以你也不用把它們寫(xiě)到 .env 文件中。

在讀取配置并連接到數(shù)據(jù)庫(kù)之后,我們會(huì)創(chuàng)建一個(gè) OAuth 配置。我們會(huì)使用 ORIGIN 信息來(lái)構(gòu)建回調(diào)  URL(就和我們?cè)?GitHub 頁(yè)面上注冊(cè)的一樣)。我們的數(shù)據(jù)范圍設(shè)置為  “read:user”。這會(huì)允許我們讀取公開(kāi)的用戶信息,這里我們只需要他的用戶名和頭像就夠了。然后我們會(huì)初始化 cookie 和 JWT  簽名器。定義一些端點(diǎn)并啟動(dòng)服務(wù)器。

在實(shí)現(xiàn) HTTP 處理程序之前,讓我們編寫(xiě)一些函數(shù)來(lái)發(fā)送 HTTP 響應(yīng)。

func respond(w http.ResponseWriter, v interface{}, statusCode int) {    b, err := json.Marshal(v)    if err != nil {        respondError(w, fmt.Errorf("could not marshal response: %v", err))        return    }    w.Header().Set("Content-Type", "application/json; charset=utf-8")    w.WriteHeader(statusCode)    w.Write(b)} func respondError(w http.ResponseWriter, err error) {    log.Println(err)    http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)}

第一個(gè)函數(shù)用來(lái)發(fā)送 JSON,而第二個(gè)將錯(cuò)誤記錄到控制臺(tái)并返回一個(gè) 500 Internal Server Error 錯(cuò)誤信息。

OAuth 開(kāi)始

所以,用戶點(diǎn)擊寫(xiě)著 “Access with GitHub” 的鏈接。該鏈接指向 /api/oauth/github,這將會(huì)把用戶重定向到 github。

func githubOAuthStart(w http.ResponseWriter, r *http.Request) {    state, err := gonanoid.Nanoid()    if err != nil {        respondError(w, fmt.Errorf("could not generte state: %v", err))        return    }     stateCookieValue, err := cookieSigner.Encode("state", state)    if err != nil {        respondError(w, fmt.Errorf("could not encode state cookie: %v", err))        return    }     http.SetCookie(w, &http.Cookie{        Name:     "state",        Value:    stateCookieValue,        Path:     "/api/oauth/github",        HttpOnly: true,    })    http.Redirect(w, r, githubOAuthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)}

OAuth3 使用一種機(jī)制來(lái)防止 CSRF ,因此它需要一個(gè)“狀態(tài)”(state)。我們使用 Nanoid() 來(lái)創(chuàng)建一個(gè)隨機(jī)字符串,并用這個(gè)字符串作為狀態(tài)。我們也把它保存為一個(gè) cookie。

OAuth 回調(diào)

一旦用戶授權(quán)我們?cè)L問(wèn)他的個(gè)人信息,他將會(huì)被重定向到這個(gè)端點(diǎn)。這個(gè) URL 的查詢字符串上將會(huì)包含狀態(tài)(state)和授權(quán)碼(code): /api/oauth/github/callback?state=&code=。

const jwtLifetime = time.Hour * 24 * 14 type GithubUser struct {    ID        int     `json:"id"`    Login     string  `json:"login"`    AvatarURL *string `json:"avatar_url,omitempty"`} type User struct {    ID        string  `json:"id"`    Username  string  `json:"username"`    AvatarURL *string `json:"avatarUrl"`} func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {    stateCookie, err := r.Cookie("state")    if err != nil {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    }     http.SetCookie(w, &http.Cookie{        Name:     "state",        Value:    "",        MaxAge:   -1,        HttpOnly: true,    })     var state string    if err = cookieSigner.Decode("state", stateCookie.Value, &state); err != nil {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    }     q := r.URL.Query()     if state != q.Get("state") {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    }     ctx := r.Context()     t, err := githubOAuthConfig.Exchange(ctx, q.Get("code"))    if err != nil {        respondError(w, fmt.Errorf("could not fetch github token: %v", err))        return    }     client := githubOAuthConfig.Client(ctx, t)    resp, err := client.Get("https://api.github.com/user")    if err != nil {        respondError(w, fmt.Errorf("could not fetch github user: %v", err))        return    }     var githubUser GithubUser    if err = json.NewDecoder(resp.Body).Decode(&githubUser); err != nil {        respondError(w, fmt.Errorf("could not decode github user: %v", err))        return    }    defer resp.Body.Close()     tx, err := db.BeginTx(ctx, nil)    if err != nil {        respondError(w, fmt.Errorf("could not begin tx: %v", err))        return    }     var user User    if err = tx.QueryRowContext(ctx, `        SELECT id, username, avatar_url FROM users WHERE github_id = $1    `, githubUser.ID).Scan(&user.ID, &user.Username, &user.AvatarURL); err == sql.ErrNoRows {        if err = tx.QueryRowContext(ctx, `            INSERT INTO users (username, avatar_url, github_id) VALUES ($1, $2, $3)            RETURNING id        `, githubUser.Login, githubUser.AvatarURL, githubUser.ID).Scan(&user.ID); err != nil {            respondError(w, fmt.Errorf("could not insert user: %v", err))            return        }        user.Username = githubUser.Login        user.AvatarURL = githubUser.AvatarURL    } else if err != nil {        respondError(w, fmt.Errorf("could not query user by github ID: %v", err))        return    }     if err = tx.Commit(); err != nil {        respondError(w, fmt.Errorf("could not commit to finish github oauth: %v", err))        return    }     exp := time.Now().Add(jwtLifetime)    token, err := jwtSigner.Encode(jwt.Claims{        Subject:    user.ID,        Expiration: json.Number(strconv.FormatInt(exp.Unix(), 10)),    })    if err != nil {        respondError(w, fmt.Errorf("could not create token: %v", err))        return    }     expiresAt, _ := exp.MarshalText()     data := make(url.Values)    data.Set("token", string(token))    data.Set("expires_at", string(expiresAt))     http.Redirect(w, r, "/callback?"+data.Encode(), http.StatusTemporaryRedirect)}

首先,我們會(huì)嘗試使用之前保存的狀態(tài)對(duì) cookie 進(jìn)行解碼。并將其與查詢字符串中的狀態(tài)進(jìn)行比較。如果它們不匹配,我們會(huì)返回一個(gè) 418 I'm teapot(未知來(lái)源)錯(cuò)誤。

接著,我們使用授權(quán)碼生成一個(gè)令牌。這個(gè)令牌被用于創(chuàng)建 HTTP 客戶端來(lái)向 GitHub API 發(fā)出請(qǐng)求。所以最終我們會(huì)向 https://api.github.com/user 發(fā)送一個(gè) GET 請(qǐng)求。這個(gè)端點(diǎn)將會(huì)以 JSON 格式向我們提供當(dāng)前經(jīng)過(guò)身份驗(yàn)證的用戶信息。我們將會(huì)解碼這些內(nèi)容,一并獲取用戶的 ID、登錄名(用戶名)和頭像 URL。

然后我們將會(huì)嘗試在數(shù)據(jù)庫(kù)上找到具有該 GitHub ID 的用戶。如果沒(méi)有找到,就使用該數(shù)據(jù)創(chuàng)建一個(gè)新的。

之后,對(duì)于新創(chuàng)建的用戶,我們會(huì)發(fā)出一個(gè)將用戶 ID 作為主題(Subject)的 JSON 網(wǎng)絡(luò)令牌,并使用該令牌重定向到前端,查詢字符串中一并包含該令牌的到期日(Expiration)。

這一 Web 應(yīng)用也會(huì)被用在其他帖子,但是重定向的鏈接會(huì)是 /callback?token=&expires_at=。在那里,我們將會(huì)利用 JavaScript 從 URL 中獲取令牌和到期日,并通過(guò) Authorization 標(biāo)頭中的令牌以 Bearer token_here 的形式對(duì) /api/auth_user 進(jìn)行 GET 請(qǐng)求,來(lái)獲取已認(rèn)證的身份用戶并將其保存到 localStorage。

Guard 中間件

為了獲取當(dāng)前已經(jīng)過(guò)身份驗(yàn)證的用戶,我們?cè)O(shè)計(jì)了 Guard 中間件。這是因?yàn)樵诮酉聛?lái)的文章中,我們會(huì)有很多需要進(jìn)行身份認(rèn)證的端點(diǎn),而中間件將會(huì)允許我們共享這一功能。

type ContextKey struct {    Name string} var keyAuthUserID = ContextKey{"auth_user_id"} func guard(handler http.HandlerFunc) http.HandlerFunc {    return func(w http.ResponseWriter, r *http.Request) {        var token string        if a := r.Header.Get("Authorization"); strings.HasPrefix(a, "Bearer ") {            token = a[7:]        } else if t := r.URL.Query().Get("token"); t != "" {            token = t        } else {            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)            return        }         var claims jwt.Claims        if err := jwtSigner.Decode([]byte(token), &claims); err != nil {            http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)            return        }         ctx := r.Context()        ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)         handler(w, r.WithContext(ctx))    }}

首先,我們嘗試從 Authorization 標(biāo)頭或者是 URL 查詢字符串中的 token 字段中讀取令牌。如果沒(méi)有找到,我們需要返回 401 Unauthorized(未授權(quán))錯(cuò)誤。然后我們將會(huì)對(duì)令牌中的申明進(jìn)行解碼,并使用該主題作為當(dāng)前已經(jīng)過(guò)身份驗(yàn)證的用戶 ID。

現(xiàn)在,我們可以用這一中間件來(lái)封裝任何需要授權(quán)的 http.handlerFunc,并且在處理函數(shù)的上下文中保有已經(jīng)過(guò)身份驗(yàn)證的用戶 ID。

var guarded = guard(func(w http.ResponseWriter, r *http.Request) {    authUserID := r.Context().Value(keyAuthUserID).(string)})

獲取認(rèn)證用戶

func getAuthUser(w http.ResponseWriter, r *http.Request) {    ctx := r.Context()    authUserID := ctx.Value(keyAuthUserID).(string)     var user User    if err := db.QueryRowContext(ctx, `        SELECT username, avatar_url FROM users WHERE id = $1    `, authUserID).Scan(&user.Username, &user.AvatarURL); err == sql.ErrNoRows {        http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)        return    } else if err != nil {        respondError(w, fmt.Errorf("could not query auth user: %v", err))        return    }     user.ID = authUserID     respond(w, user, http.StatusOK)}

我們使用 Guard 中間件來(lái)獲取當(dāng)前經(jīng)過(guò)身份認(rèn)證的用戶 ID 并查詢數(shù)據(jù)庫(kù)。

以上就是關(guān)于“OAuth如何構(gòu)建一個(gè)即時(shí)消息應(yīng)用”這篇文章的內(nèi)容,相信大家都有了一定的了解,希望小編分享的內(nèi)容對(duì)大家有幫助,若想了解更多相關(guān)的知識(shí)內(nèi)容,請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。

網(wǎng)頁(yè)名稱(chēng):OAuth如何構(gòu)建一個(gè)即時(shí)消息應(yīng)用
文章位置:http://muchs.cn/article2/jpieic.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站策劃面包屑導(dǎo)航、網(wǎng)站排名網(wǎng)站導(dǎo)航、云服務(wù)器、小程序開(kāi)發(fā)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

手機(jī)網(wǎng)站建設(shè)