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

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

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

什么是OAuth2刁愿?

Open Authorization Version 2.0稱為OAuth2绰寞。它是一種保護RESTful Web服務(wù)的協(xié)議或框架。OAuth2非常強大∠晨冢現(xiàn)在滤钱,大多數(shù)REST API都受到OAuth2的保護,因為它具有堅固的安全性脑题。

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將檢查它是否屬實并要求該用戶進行授權(quán)。然后github將向客戶端發(fā)出一個短暫的令牌全谤。

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

“哦补憾,訪問令牌看起來很熟悉,可能是我們已經(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ù)器 - 具有受保護資源的服務(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)。現(xiàn)在你想讓它打開盾碗,以便公眾可以使用它媚污,或者你想用它賺錢。

無論是什么情況廷雅,您都需要保護您的資源免受未經(jīng)授權(quán)的訪問或惡意攻擊耗美。為此,您需要保護您的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)阁谆。

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

雖然端點名稱受到保護,但任何人都可以訪問它场绿。所以我們需要用OAuth2來保護它剖效。

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

路線

  1. /credentials用于發(fā)出客戶機憑據(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)建了兩個隨機字符串熬拒,一個用于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ù)器可以解碼請求有效負載中的所有必要數(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。但是我們/受保護的路線仍然沒有受到保護怯晕。我們需要設(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ā)送到受保護的端點:

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)系作者
  • 序言:七十年代末跃脊,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子苛吱,更是在濱河造成了極大的恐慌酪术,老刑警劉巖,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翠储,死亡現(xiàn)場離奇詭異绘雁,居然都是意外死亡,警方通過查閱死者的電腦和手機援所,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門咧七,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事废酷∽垦校” “怎么了寥闪?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵缚柳,是天一觀的道長彩掐。 經(jīng)常有香客問我,道長裁奇,這世上最難降的妖魔是什么溃肪? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘俺猿。我一直安慰自己伯病,他們只是感情好午笛,可當(dāng)我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布惭蟋。 她就那樣靜靜地躺著,像睡著了一般药磺。 火紅的嫁衣襯著肌膚如雪告组。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天癌佩,我揣著相機與錄音木缝,去河邊找鬼。 笑死围辙,一個胖子當(dāng)著我的面吹牛我碟,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姚建,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼矫俺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了掸冤?” 一聲冷哼從身側(cè)響起厘托,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎稿湿,沒想到半個月后铅匹,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡饺藤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年包斑,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片涕俗。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡舰始,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咽袜,到底是詐尸還是另有隱情,我是刑警寧澤枕稀,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布询刹,位于F島的核電站,受9級特大地震影響萎坷,放射性物質(zhì)發(fā)生泄漏凹联。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一哆档、第九天 我趴在偏房一處隱蔽的房頂上張望蔽挠。 院中可真熱鬧,春花似錦、人聲如沸澳淑。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽杠巡。三九已至量窘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間氢拥,已是汗流浹背蚌铜。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留嫩海,地道東北人冬殃。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像叁怪,于是被迫代替她去往敵國和親审葬。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,446評論 2 348

推薦閱讀更多精彩內(nèi)容