日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
構(gòu)建一個(gè)即時(shí)消息應(yīng)用(二):OAuth

上一篇:模式。

創(chuàng)新互聯(lián)建站主營平泉網(wǎng)站建設(shè)的網(wǎng)絡(luò)公司,主營網(wǎng)站建設(shè)方案,App定制開發(fā),平泉h5重慶小程序開發(fā)搭建,平泉網(wǎng)站營銷推廣歡迎平泉等地區(qū)企業(yè)咨詢

在這篇帖子中,我們將會(huì)通過為應(yīng)用添加社交登錄功能進(jìn)入后端開發(fā)。

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

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

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

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

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

 
 
 
  1. package main
  2.  
  3. import (
  4. "database/sql"
  5. "fmt"
  6. "log"
  7. "net/http"
  8. "net/url"
  9. "os"
  10. "strconv"
  11.  
  12. "github.com/gorilla/securecookie"
  13. "github.com/joho/godotenv"
  14. "github.com/knq/jwt"
  15. _ "github.com/lib/pq"
  16. "github.com/matryer/way"
  17. "golang.org/x/oauth2"
  18. "golang.org/x/oauth2/github"
  19. )
  20.  
  21. var origin *url.URL
  22. var db *sql.DB
  23. var githubOAuthConfig *oauth2.Config
  24. var cookieSigner *securecookie.SecureCookie
  25. var jwtSigner jwt.Signer
  26.  
  27. func main() {
  28. godotenv.Load()
  29.  
  30. port := intEnv("PORT", 3000)
  31. originString := env("ORIGIN", fmt.Sprintf("http://localhost:%d/", port))
  32. databaseURL := env("DATABASE_URL", "postgresql://root@127.0.0.1:26257/messenger?sslmode=disable")
  33. githubClientID := os.Getenv("GITHUB_CLIENT_ID")
  34. githubClientSecret := os.Getenv("GITHUB_CLIENT_SECRET")
  35. hashKey := env("HASH_KEY", "secret")
  36. jwtKey := env("JWT_KEY", "secret")
  37.  
  38. var err error
  39. if origin, err = url.Parse(originString); err != nil || !origin.IsAbs() {
  40. log.Fatal("invalid origin")
  41. return
  42. }
  43.  
  44. if i, err := strconv.Atoi(origin.Port()); err == nil {
  45. port = i
  46. }
  47.  
  48. if githubClientID == "" || githubClientSecret == "" {
  49. log.Fatalf("remember to set both $GITHUB_CLIENT_ID and $GITHUB_CLIENT_SECRET")
  50. return
  51. }
  52.  
  53. if db, err = sql.Open("postgres", databaseURL); err != nil {
  54. log.Fatalf("could not open database connection: %v\n", err)
  55. return
  56. }
  57. defer db.Close()
  58. if err = db.Ping(); err != nil {
  59. log.Fatalf("could not ping to db: %v\n", err)
  60. return
  61. }
  62.  
  63. githubRedirectURL := *origin
  64. githubRedirectURL.Path = "/api/oauth/github/callback"
  65. githubOAuthConfig = &oauth2.Config{
  66. ClientID: githubClientID,
  67. ClientSecret: githubClientSecret,
  68. Endpoint: github.Endpoint,
  69. RedirectURL: githubRedirectURL.String(),
  70. Scopes: []string{"read:user"},
  71. }
  72.  
  73. cookieSigner = securecookie.New([]byte(hashKey), nil).MaxAge(0)
  74.  
  75. jwtSigner, err = jwt.HS256.New([]byte(jwtKey))
  76. if err != nil {
  77. log.Fatalf("could not create JWT signer: %v\n", err)
  78. return
  79. }
  80.  
  81. router := way.NewRouter()
  82. router.HandleFunc("GET", "/api/oauth/github", githubOAuthStart)
  83. router.HandleFunc("GET", "/api/oauth/github/callback", githubOAuthCallback)
  84. router.HandleFunc("GET", "/api/auth_user", guard(getAuthUser))
  85.  
  86. log.Printf("accepting connections on port %d\n", port)
  87. log.Printf("starting server at %s\n", origin.String())
  88. addr := fmt.Sprintf(":%d", port)
  89. if err = http.ListenAndServe(addr, router); err != nil {
  90. log.Fatalf("could not start server: %v\n", err)
  91. }
  92. }
  93.  
  94. func env(key, fallbackValue string) string {
  95. v, ok := os.LookupEnv(key)
  96. if !ok {
  97. return fallbackValue
  98. }
  99. return v
  100. }
  101.  
  102. func intEnv(key string, fallbackValue int) int {
  103. v, ok := os.LookupEnv(key)
  104. if !ok {
  105. return fallbackValue
  106. }
  107. i, err := strconv.Atoi(v)
  108. if err != nil {
  109. return fallbackValue
  110. }
  111. return i
  112. }

安裝依賴項(xiàng):

 
 
 
  1. go get -u github.com/gorilla/securecookie
  2. go get -u github.com/joho/godotenv
  3. go get -u github.com/knq/jwt
  4. go get -u github.com/lib/pq
  5. ge get -u github.com/matoous/go-nanoid
  6. go get -u github.com/matryer/way
  7. go get -u golang.org/x/oauth2

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

 
 
 
  1. GITHUB_CLIENT_ID=your_github_client_id
  2. GITHUB_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ù)庫的地址。默認(rèn)值是 postgresql://root@127.0.0.1:26257/messenger?sslmode=disable。
  • HASH_KEY:用于為 cookie 簽名的密鑰。沒錯(cuò),我們會(huì)使用已簽名的 cookie 來確保安全。
  • JWT_KEY:用于簽署 JSON 網(wǎng)絡(luò)令牌Web Token的密鑰。

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

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

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

 
 
 
  1. func respond(w http.ResponseWriter, v interface{}, statusCode int) {
  2. b, err := json.Marshal(v)
  3. if err != nil {
  4. respondError(w, fmt.Errorf("could not marshal response: %v", err))
  5. return
  6. }
  7. w.Header().Set("Content-Type", "application/json; charset=utf-8")
  8. w.WriteHeader(statusCode)
  9. w.Write(b)
  10. }
  11.  
  12. func respondError(w http.ResponseWriter, err error) {
  13. log.Println(err)
  14. http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
  15. }

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

OAuth 開始

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

 
 
 
  1. func githubOAuthStart(w http.ResponseWriter, r *http.Request) {
  2. state, err := gonanoid.Nanoid()
  3. if err != nil {
  4. respondError(w, fmt.Errorf("could not generte state: %v", err))
  5. return
  6. }
  7.  
  8. stateCookieValue, err := cookieSigner.Encode("state", state)
  9. if err != nil {
  10. respondError(w, fmt.Errorf("could not encode state cookie: %v", err))
  11. return
  12. }
  13.  
  14. http.SetCookie(w, &http.Cookie{
  15. Name: "state",
  16. Value: stateCookieValue,
  17. Path: "/api/oauth/github",
  18. HttpOnly: true,
  19. })
  20. http.Redirect(w, r, githubOAuthConfig.AuthCodeURL(state), http.StatusTemporaryRedirect)
  21. }

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

OAuth 回調(diào)

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

 
 
 
  1. const jwtLifetime = time.Hour * 24 * 14
  2.  
  3. type GithubUser struct {
  4. ID int `json:"id"`
  5. Login string `json:"login"`
  6. AvatarURL *string `json:"avatar_url,omitempty"`
  7. }
  8.  
  9. type User struct {
  10. ID string `json:"id"`
  11. Username string `json:"username"`
  12. AvatarURL *string `json:"avatarUrl"`
  13. }
  14.  
  15. func githubOAuthCallback(w http.ResponseWriter, r *http.Request) {
  16. stateCookie, err := r.Cookie("state")
  17. if err != nil {
  18. http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
  19. return
  20. }
  21.  
  22. http.SetCookie(w, &http.Cookie{
  23. Name: "state",
  24. Value: "",
  25. MaxAge: -1,
  26. HttpOnly: true,
  27. })
  28.  
  29. var state string
  30. if err = cookieSigner.Decode("state", stateCookie.Value, &state); err != nil {
  31. http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
  32. return
  33. }
  34.  
  35. q := r.URL.Query()
  36.  
  37. if state != q.Get("state") {
  38. http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
  39. return
  40. }
  41.  
  42. ctx := r.Context()
  43.  
  44. t, err := githubOAuthConfig.Exchange(ctx, q.Get("code"))
  45. if err != nil {
  46. respondError(w, fmt.Errorf("could not fetch github token: %v", err))
  47. return
  48. }
  49.  
  50. client := githubOAuthConfig.Client(ctx, t)
  51. resp, err := client.Get("https://api.github.com/user")
  52. if err != nil {
  53. respondError(w, fmt.Errorf("could not fetch github user: %v", err))
  54. return
  55. }
  56.  
  57. var githubUser GithubUser
  58. if err = json.NewDecoder(resp.Body).Decode(&githubUser); err != nil {
  59. respondError(w, fmt.Errorf("could not decode github user: %v", err))
  60. return
  61. }
  62. defer resp.Body.Close()
  63.  
  64. tx, err := db.BeginTx(ctx, nil)
  65. if err != nil {
  66. respondError(w, fmt.Errorf("could not begin tx: %v", err))
  67. return
  68. }
  69.  
  70. var user User
  71. if err = tx.QueryRowContext(ctx, `
  72. SELECT id, username, avatar_url FROM users WHERE github_id = $1
  73. `, githubUser.ID).Scan(&user.ID, &user.Username, &user.AvatarURL); err == sql.ErrNoRows {
  74. if err = tx.QueryRowContext(ctx, `
  75. INSERT INTO users (username, avatar_url, github_id) VALUES ($1, $2, $3)
  76. RETURNING id
  77. `, githubUser.Login, githubUser.AvatarURL, githubUser.ID).Scan(&user.ID); err != nil {
  78. respondError(w, fmt.Errorf("could not insert user: %v", err))
  79. return
  80. }
  81. user.Username = githubUser.Login
  82. user.AvatarURL = githubUser.AvatarURL
  83. } else if err != nil {
  84. respondError(w, fmt.Errorf("could not query user by github ID: %v", err))
  85. return
  86. }
  87.  
  88. if err = tx.Commit(); err != nil {
  89. respondError(w, fmt.Errorf("could not commit to finish github oauth: %v", err))
  90. return
  91. }
  92.  
  93. exp := time.Now().Add(jwtLifetime)
  94. token, err := jwtSigner.Encode(jwt.Claims{
  95. Subject: user.ID,
  96. Expiration: json.Number(strconv.FormatInt(exp.Unix(), 10)),
  97. })
  98. if err != nil {
  99. respondError(w, fmt.Errorf("could not create token: %v", err))
  100. return
  101. }
  102.  
  103. expiresAt, _ := exp.MarshalText()
  104.  
  105. data := make(url.Values)
  106. data.Set("token", string(token))
  107. data.Set("expires_at", string(expiresAt))
  108.  
  109. http.Redirect(w, r, "/callback?"+data.Encode(), http.StatusTemporaryRedirect)
  110. }

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

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

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

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

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

Guard 中間件

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

 
 
 
  1. type ContextKey struct {
  2. Name string
  3. }
  4.  
  5. var keyAuthUserID = ContextKey{"auth_user_id"}
  6.  
  7. func guard(handler http.HandlerFunc) http.HandlerFunc {
  8. return func(w http.ResponseWriter, r *http.Request) {
  9. var token string
  10. if a := r.Header.Get("Authorization"); strings.HasPrefix(a, "Bearer ") {
  11. token = a[7:]
  12. } else if t := r.URL.Query().Get("token"); t != "" {
  13. token = t
  14. } else {
  15. http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
  16. return
  17. }
  18.  
  19. var claims jwt.Claims
  20. if err := jwtSigner.Decode([]byte(token), &claims); err != nil {
  21. http.Error(w, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
  22. return
  23. }
  24.  
  25. ctx := r.Context()
  26. ctx = context.WithValue(ctx, keyAuthUserID, claims.Subject)
  27.  
  28. handler(w, r.WithContext(ctx))
  29. }
  30. }

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

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

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

獲取認(rèn)證用戶

 
 
 
  1. func getAuthUser(w http.ResponseWriter, r *http.Request) {
  2. ctx := r.Context()
  3. authUserID := ctx.Value(keyAuthUserID).(string)
  4.  
  5. var user User
  6. if err := db.QueryRowContext(ctx, `
  7. SELECT username, avatar_url FROM users WHERE id = $1
  8. `, authUserID).Scan(&user.Username, &user.AvatarURL); err == sql.ErrNoRows {
  9. http.Error(w, http.StatusText(http.StatusTeapot), http.StatusTeapot)
  10. return
  11. } else if err != nil {
  12. respondError(w, fmt.Errorf("could not query auth user: %v", err))
  13. return
  14. }
  15.  
  16. user.ID = authUserID
  17.  
  18. respond(w, user, http.StatusOK)
  19. }

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

這一部分涵蓋了后端的 OAuth 流程。在下一篇帖子中,我們將會(huì)看到如何開始與其他用戶的對話。

  • 源代碼

分享題目:構(gòu)建一個(gè)即時(shí)消息應(yīng)用(二):OAuth
本文路徑:http://www.dlmjj.cn/article/cdgegsj.html