使用Golang構(gòu)建你的OAuth2服務(wù)

您好,在今天的文章中,我將向您展示如何構(gòu)建自己的OAuth2服務(wù)器,就像google,facebook,github等。

如果您想構(gòu)建生產(chǎn)就緒的公共或私有API,這將非常有用。所以讓我們開始吧。

什么是OAuth2?

Open Authorization Version 2.0稱為OAuth2。它是一種保護(hù)RESTful Web服務(wù)的協(xié)議或框架。OAuth2非常強(qiáng)大?,F(xiàn)在,大多數(shù)REST API都受到OAuth2的保護(hù),因為它具有堅固的安全性。

OAuth2有兩個部分

01.客戶

02.服務(wù)器

OAuth2客戶端

如果你熟悉這個屏幕,你知道我在說什么。無論如何,讓我解釋一下圖像背后的故事:

您正在構(gòu)建面向用戶的應(yīng)用程序,該應(yīng)用程序與用戶的github存儲庫一起使用。例如:CI工具,如TravisCI,CircleCI,Drone等。

但是用戶的github帳戶是安全的,如果所有者不想要,則沒有人可以訪問它。那么這些CI工具如何訪問用戶的github帳戶和存儲庫呢?

簡單。

您的應(yīng)用程序?qū)⒃儐栍脩?/p>

“為了與我們合作,您需要為您的github存儲庫提供讀取權(quán)限。你同意嗎?”

然后用戶會說

“是的,我愿意。并做任何需要做的事情。

然后,您的應(yīng)用程序?qū)⒙?lián)系github的權(quán)限,以授予對該特定用戶的github帳戶的訪問權(quán)限。Github將檢查它是否屬實并要求該用戶進(jìn)行授權(quán)。然后github將向客戶端發(fā)出一個短暫的令牌。

現(xiàn)在,當(dāng)您的應(yīng)用程序需要在身份驗證和授權(quán)后訪問它時,它需要發(fā)送帶有請求的訪問令牌,以便github會認(rèn)為:

“哦,訪問令牌看起來很熟悉,可能是我們已經(jīng)給你了。好的,你可以訪問“

那是漫長的故事。天已經(jīng)改變,現(xiàn)在你不需要每次都去github權(quán)限(我們從來沒有這樣做過)。一切都可以自動完成。

但是怎么樣?

image.png

這是我?guī)追昼娗罢勥^的UML序列圖。只是圖形表示。

從上圖中,我們發(fā)現(xiàn)了一些重要的事情。

OAuth2有4個角色:

01.用戶 - 將使用您的應(yīng)用程序的最終用戶

02.客戶端 - 您正在構(gòu)建的應(yīng)用程序?qū)⑹褂胓ithub帳戶并且用戶將使用該應(yīng)用程序

  1. Auth Server - 處理主要OAuth事務(wù)的服務(wù)器

04.資源服務(wù)器 - 具有受保護(hù)資源的服務(wù)器。例如github

客戶端代表用戶向auth服務(wù)器發(fā)送OAuth2請求。

構(gòu)建OAuth2客戶端既不容易也不困難。聽起來很有趣,對嗎?我們將在下一部分中做到這一點。

但在這一部分,我們將走向世界的另一端。我們將構(gòu)建自己的OAuth2服務(wù)器。哪個不容易但多汁。

準(zhǔn)備?我們走吧

OAuth2服務(wù)器

你可能會問我

“等一下Cyan,為什么要建一個OAuth2服務(wù)器?”

你忘記了嗎?我早些時候已經(jīng)說過了。好的,我再告訴你。

想象一下,您正在構(gòu)建一個非常有用的應(yīng)用程序,可以提供準(zhǔn)確的天氣信息(這里有很多這樣的api)?,F(xiàn)在你想讓它打開,以便公眾可以使用它,或者你想用它賺錢。

無論是什么情況,您都需要保護(hù)您的資源免受未經(jīng)授權(quán)的訪問或惡意攻擊。為此,您需要保護(hù)您的API資源。這是OAuth2的事情。答對了!

從上圖中,我們可以看到我們需要在REST API資源服務(wù)器前放置一個Auth服務(wù)器。這就是我們所說的。Auth服務(wù)器將使用OAuth2規(guī)范構(gòu)建。然后我們將成為第一張照片的github,哈哈哈開玩笑。

OAuth2服務(wù)器的主要目標(biāo)是為客戶端提供訪問令牌。這就是為什么OAuth2 Server也稱為OAuth2 Provider,因為它們提供令牌。

夠說話了。

基于授權(quán)流類型有四種類型的OAuth2服務(wù)器:

01.授權(quán)代碼授權(quán)

02.隱式授予

03.客戶證書授予

04.密碼授予

如果您想了解有關(guān)OAuth2的更多信息,請查看這篇精彩的文章。

對于本文,我們將使用客戶端憑據(jù)授予類型。所以讓我們深入研究

客戶端憑據(jù)授予基于流的服務(wù)器

在實施基于客戶端憑據(jù)授權(quán)流程的OAuth2服務(wù)器時,我們需要了解一些事情。

在此授權(quán)類型中,沒有用戶交互(即注冊,登錄)。需要兩件事,它們是client_idclient_secret。有了這兩件事,我們就可以獲得access_token了。客戶是第三方應(yīng)用程序。當(dāng)您需要在沒有用戶或僅由客戶端應(yīng)用程序訪問資源服務(wù)器時,此授權(quán)類型很簡單且最適合。

image.png

這是它的UML序列圖。

編碼

為了構(gòu)建這個,我們需要依賴一個很棒的Go包

首先,讓我們構(gòu)建一個簡單的API服務(wù)器作為資源服務(wù)器

main.go

package main

import (
    "log"
    "net/http"
)

func main() {
    http.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, I'm protected"))
    })

    log.Fatal(http.ListenAndServe(":9096", nil))

運行服務(wù)器并將獲取請求發(fā)送到http://localhost:9096/protected

你會得到回應(yīng)。

它是什么樣的受保護(hù)服務(wù)器?

雖然端點名稱受到保護(hù),但任何人都可以訪問它。所以我們需要用OAuth2來保護(hù)它。

現(xiàn)在我們將編寫授權(quán)服務(wù)器

路線

  1. /credentials用于發(fā)出客戶機(jī)憑據(jù)(client_id和client_secret)

  2. / token發(fā)出帶有客戶端憑據(jù)的令牌

我們需要實現(xiàn)這兩條路線。

這是初步設(shè)置

main.go

package main

import (
    "encoding/json"
    "fmt"
    "github.com/google/uuid"
    "gopkg.in/oauth2.v3/models"
    "log"
    "net/http"
    "time"

    "gopkg.in/oauth2.v3/errors"
    "gopkg.in/oauth2.v3/manage"
    "gopkg.in/oauth2.v3/server"
    "gopkg.in/oauth2.v3/store"
)

func main() {
   manager := manage.NewDefaultManager()
   manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)

   manager.MustTokenStorage(store.NewMemoryTokenStore())

   clientStore := store.NewClientStore()
   manager.MapClientStorage(clientStore)

   srv := server.NewDefaultServer(manager)
   srv.SetAllowGetAccessRequest(true)
   srv.SetClientInfoHandler(server.ClientFormHandler)
   manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)

   srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
      log.Println("Internal Error:", err.Error())
      return
   })

   srv.SetResponseErrorHandler(func(re *errors.Response) {
      log.Println("Response Error:", re.Error.Error())
   })
    
   http.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, I'm protected"))
   })

   log.Fatal(http.ListenAndServe(":9096", nil))
}

在這里,我們創(chuàng)建了一個管理器,客戶端存儲和auth服務(wù)器本身。

這是/credentials路由

http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
   clientId := uuid.New().String()[:8]
   clientSecret := uuid.New().String()[:8]
   err := clientStore.Set(clientId, &models.Client{
      ID:     clientId,
      Secret: clientSecret,
      Domain: "http://localhost:9094",
   })
   if err != nil {
      fmt.Println(err.Error())
   }

   w.Header().Set("Content-Type", "application/json")
   json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
})

它創(chuàng)建了兩個隨機(jī)字符串,一個用于client_id,另一個用于client_secret。然后將它們保存到客戶端存儲中。并將它們作為回應(yīng)返回。而已。我們在內(nèi)存商店中使用過,但我們可以將它們存儲在redis,mongodb,postgres等中。

這是/ token路由:

http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
   srv.HandleTokenRequest(w, r)
})

這很簡單。它將請求和響應(yīng)傳遞給適當(dāng)?shù)奶幚沓绦?,以便服?wù)器可以解碼請求有效負(fù)載中的所有必要數(shù)據(jù)。

所以這是我們的整體代碼:

package main

import (
   "encoding/json"
   "fmt"
   "github.com/google/uuid"
   "gopkg.in/oauth2.v3/models"
   "log"
   "net/http"
   "time"

   "gopkg.in/oauth2.v3/errors"
   "gopkg.in/oauth2.v3/manage"
   "gopkg.in/oauth2.v3/server"
   "gopkg.in/oauth2.v3/store"
)

func main() {
   manager := manage.NewDefaultManager()
   manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)

   manager.MustTokenStorage(store.NewMemoryTokenStore())

   clientStore := store.NewClientStore()
   manager.MapClientStorage(clientStore)

   srv := server.NewDefaultServer(manager)
   srv.SetAllowGetAccessRequest(true)
   srv.SetClientInfoHandler(server.ClientFormHandler)
   manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)

   srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
      log.Println("Internal Error:", err.Error())
      return
   })

   srv.SetResponseErrorHandler(func(re *errors.Response) {
      log.Println("Response Error:", re.Error.Error())
   })

   http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
      srv.HandleTokenRequest(w, r)
   })

   http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
      clientId := uuid.New().String()[:8]
      clientSecret := uuid.New().String()[:8]
      err := clientStore.Set(clientId, &models.Client{
         ID:     clientId,
         Secret: clientSecret,
         Domain: "http://localhost:9094",
      })
      if err != nil {
         fmt.Println(err.Error())
      }

      w.Header().Set("Content-Type", "application/json")
      json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
   })
   
   http.HandleFunc("/protected", func(w http.ResponseWriter, r *http.Request) {
        w.Write([]byte("Hello, I'm protected"))
   })
   log.Fatal(http.ListenAndServe(":9096", nil))
}

運行代碼并轉(zhuǎn)到http://localhost:9096/credentials route以注冊并獲取client_id和client_secret

現(xiàn)在轉(zhuǎn)到此URL http:// localhost:9096/token?grant_type=client_credentials&client_id = YOUR_CLIENT_ID&client_secret=YOUR_CLIENT_SECRET&scope = all

您將獲得具有到期時間和其他一些信息的access_token。

現(xiàn)在我們得到了access_token。但是我們/受保護(hù)的路線仍然沒有受到保護(hù)。我們需要設(shè)置一種方法來檢查每個客戶端請求是否存在有效令牌。如果是,那么我們給客戶端訪問權(quán)限。否則不是。

我們可以用中間件來做到這一點。

如果您知道自己在做什么,那么在go中編寫中間件會非常有趣。這是中間件:

func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      _, err := srv.ValidationBearerToken(r)
      if err != nil {
         http.Error(w, err.Error(), http.StatusBadRequest)
         return
      }

      f.ServeHTTP(w, r)
   })
}

這將檢查是否為請求提供了有效的令牌,并根據(jù)該令牌采取行動。

現(xiàn)在我們需要配置/protected路由

http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
   w.Write([]byte("Hello, I'm protected"))
}, srv))

現(xiàn)在整個代碼看起來像這樣:

package main

import (
   "encoding/json"
   "fmt"
   "github.com/google/uuid"
   "gopkg.in/oauth2.v3/models"
   "log"
   "net/http"
   "time"

   "gopkg.in/oauth2.v3/errors"
   "gopkg.in/oauth2.v3/manage"
   "gopkg.in/oauth2.v3/server"
   "gopkg.in/oauth2.v3/store"
)

func main() {
   manager := manage.NewDefaultManager()
   manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg)

   // token memory store
   manager.MustTokenStorage(store.NewMemoryTokenStore())

   // client memory store
   clientStore := store.NewClientStore()
   
   manager.MapClientStorage(clientStore)

   srv := server.NewDefaultServer(manager)
   srv.SetAllowGetAccessRequest(true)
   srv.SetClientInfoHandler(server.ClientFormHandler)
   manager.SetRefreshTokenCfg(manage.DefaultRefreshTokenCfg)

   srv.SetInternalErrorHandler(func(err error) (re *errors.Response) {
      log.Println("Internal Error:", err.Error())
      return
   })

   srv.SetResponseErrorHandler(func(re *errors.Response) {
      log.Println("Response Error:", re.Error.Error())
   })

   http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
      srv.HandleTokenRequest(w, r)
   })

   http.HandleFunc("/credentials", func(w http.ResponseWriter, r *http.Request) {
      clientId := uuid.New().String()[:8]
      clientSecret := uuid.New().String()[:8]
      err := clientStore.Set(clientId, &models.Client{
         ID:     clientId,
         Secret: clientSecret,
         Domain: "http://localhost:9094",
      })
      if err != nil {
         fmt.Println(err.Error())
      }

      w.Header().Set("Content-Type", "application/json")
      json.NewEncoder(w).Encode(map[string]string{"CLIENT_ID": clientId, "CLIENT_SECRET": clientSecret})
   })

   http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
      w.Write([]byte("Hello, I'm protected"))
   }, srv))

   log.Fatal(http.ListenAndServe(":9096", nil))
}

func validateToken(f http.HandlerFunc, srv *server.Server) http.HandlerFunc {
   return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
      _, err := srv.ValidationBearerToken(r)
      if err != nil {
         http.Error(w, err.Error(), http.StatusBadRequest)
         return
      }

      f.ServeHTTP(w, r)
   })
}

現(xiàn)在運行服務(wù)器并嘗試訪問/ protected端點,而不將access_token作為URL Query。然后嘗試給出錯誤的access_token。無論哪種方式,auth服務(wù)器都會阻止你。

現(xiàn)在再次從服務(wù)器獲取憑據(jù)access_token,并將請求發(fā)送到受保護(hù)的端點:

HTTP://localhost:9096/test?access_token = YOUR_ACCESS_TOKEN

答對了!你可以訪問它。

所以我們已經(jīng)學(xué)會了如何使用Go設(shè)置我們自己的OAuth2服務(wù)器。

在下一部分中,我們將在Go中構(gòu)建OAuth2客戶端。在最后一部分中,我們將使用用戶登錄和授權(quán)構(gòu)建授權(quán)代碼授予類型的服務(wù)器。

轉(zhuǎn)自:https://hackernoon.com/build-your-own-oauth2-server-in-go-7d0f660732c3

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容