您好,在今天的文章中干奢,我將向您展示如何構(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)限(我們從來沒有這樣做過)削饵。一切都可以自動完成。
但是怎么樣未巫?
這是我?guī)追昼娗罢勥^的UML序列圖窿撬。只是圖形表示。
從上圖中叙凡,我們發(fā)現(xiàn)了一些重要的事情劈伴。
OAuth2有4個角色:
01.用戶 - 將使用您的應(yīng)用程序的最終用戶
02.客戶端 - 您正在構(gòu)建的應(yīng)用程序?qū)⑹褂胓ithub帳戶并且用戶將使用該應(yīng)用程序
- 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_id和client_secret髓梅。有了這兩件事拟蜻,我們就可以獲得access_token了】荻觯客戶是第三方應(yīng)用程序酝锅。當(dāng)您需要在沒有用戶或僅由客戶端應(yīng)用程序訪問資源服務(wù)器時,此授權(quán)類型很簡單且最適合奢方。
這是它的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ù)器
路線
/credentials用于發(fā)出客戶機憑據(jù)(client_id和client_secret)
/ 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