- 原文地址:Build your Own OAuth2 Server in Go: Client Credentials Grant Flow
- 原文做者:Cyan Tarek
- 譯文出自:掘金翻譯計劃
- 本文永久連接:github.com/xitu/gold-m…
- 譯者:shixi-li
- 校對者:JackEggie, LucaslEliane
嗨,在今天的文章中,我會向你們展現怎麼構建屬於每一個人本身的 OAuth2 服務器,就像 google、facebook 和 github 等公司同樣。前端
若是你想構建用於生產環境的公共或者私有 API,這都會是頗有幫助的。因此如今讓咱們開始吧。android
開放受權版本 2.0 被稱爲 OAuth2。它是一種保護 RESTful Web 服務的協議或者說是框架。OAuth2 很是強大。因爲 OAuth2 堅如磐石的安全性,因此如今大多數的 REST API 都經過 OAuth2 進行保護。ios
客戶端git
服務端github
若是你熟悉這個界面,你就會知道我將要說什麼。可是不管熟悉與否,都讓我來說一下這個圖片背後的故事吧。golang
你正在構建一個面向用戶的應用程序,它是與用戶的 github 倉庫協同使用的。好比:就像是 TravisCI、CircleCI 和 Drone 等 CI 工具。redis
可是用戶的 github 帳戶是被保護的,若是全部者不肯意任何人都無權訪問。那麼這些 CI 工具如何訪問用戶的 github 賬戶和倉庫的呢?mongodb
這其實很簡單。json
你的應用程序會詢問用戶後端
「爲了與咱們的服務協做,咱們須要獲得你的 github 倉庫的讀取權限。你贊成嗎?」
而後這個用戶就會說
「我贊成。大家能夠去作大家須要作的事兒啦。"
而後你的應用程序會請求 github 的權限管理以得到那個特定用戶的 github 訪問權限。Github 會檢查是否屬實並要求該用戶進行受權。經過以後 github 就會給這個客戶端發送一個臨時的令牌。
如今,當你的應用程序獲得身份驗證和受權之後須要訪問 github 時,就須要把這個令牌在請求中間帶過去,github 收到了以後就會想:
「咦,這個訪問令牌看起來很眼熟嘛,應該是咱們以前就給過你了。好,你能夠訪問了」
這是一個很長的流程。可是時代已經變啦,如今你不用每次都去 github 受權中心(固然咱們歷來也不須要這樣)。每件事均可以自動化地完成。
可是怎麼完成呢?
這是我前幾分鐘討論的內容所對應的 UML 時序圖。就是一個對應的圖形表示。
從上圖中,咱們能夠發現幾點重要的東西。
OAuth2 有 4 個角色:
用戶 — 最終使用你的應用程序的用戶
客戶端 — 就是你構建的那個會使用 github 帳戶的應用程序,也就是用戶會使用的東西
鑑權服務器 — 這個服務器主要處理 OAuth 相關事務
資源服務器 — 這個服務器有那些被保護的資源。好比說 github
客戶端表明用戶向鑑權服務器發送 OAuth2 請求。
構建一個 OAuth2 客戶端不算簡單但也不算困難。聽起來頗有趣對吧?咱們會在下一個部分來實際操做。
但在這個部分,咱們會去這個世界的另外一面看看。咱們會構建咱們本身的 OAuth2 服務端。這並不簡單可是頗有趣。
準備好了嗎?讓咱們開始吧
你也許會問我
「Cyan 等一下,爲何要構建一個 OAuth2 服務器啊?」
朋友你忘了嗎?我以前說了這一點的啊。好吧,讓我再次告訴你。
想象一下,你構建了一個很是棒的應用程序,它能夠提供準確的天氣信息(如今已經有不少這種類型的 API 了)。如今你但願把它變得開放讓公衆均可以使用或者你想靠它來賺錢了。
但不管什麼狀況,你都須要保護你的資源免受未經受權的訪問或者惡意的攻擊。 因此你須要保護你的 API 資源。那這裏就須要用到 OAuth2 啦。對吧!
從上圖中咱們能夠看到,鑑權服務器須要放置在 REST API 資源服務器以前。這就是咱們要討論的東西。這個鑑權服務器須要根據 OAuth2 規範構建。而後咱們就會變成第一張圖片裏面的 github 啦,哈哈哈哈開玩笑的。
OAuth2 服務器的主要目標是給客戶端提供訪問的令牌。這也就是爲何 OAuth2 服務器也被稱做 OAuth2 提供者,由於他們能夠提供令牌。
這個解釋就說這麼多啦。
基於鑑權流程有 4 種不一樣的 OAuth2 服務器模式:
受權碼模式
隱式受權模式
客戶端驗證模式
密碼模式
若是你想了解更多關於 OAuth2 的東西,請看 這裏的 精彩文章。
在本文中,咱們會使用 客戶端驗證模式。我們來深刻了解一下吧。
在構建基於 OAuth2 服務器的客戶端憑據受權流程時,咱們須要瞭解一些東西。
在這個受權類型裏面沒有用戶交互 (也就是指沒有註冊,登陸)。而是須要兩個東西,它們是 客戶端 ID 和 客戶端密鑰。有了這兩個東西,咱們就能夠獲取到 訪問令牌。客戶端就是第三方的應用程序。當須要在沒有用戶機制或者是僅經過客戶端應用程序,想要訪問資源服務器的時候,這種受權方式是簡便且適合的。
這就是對應的 UML 時序圖。
爲了構建這個項目,咱們須要依賴一個很是棒的 Go 語言包。
首先,咱們須要開發一個簡單的 API 服務做爲資源服務器。
package main
import (
"log"
"net/http"
)
func main() {
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))
}
複製代碼
運行這個服務而且發送 Get 請求到 http://localhost:9096/protected
你會獲得響應。
這個服務受到什麼類型的保護呢?
即便將這個接口的名字定義爲 protected,可是任何人均可以請求它。咱們須要將這個接口使用 OAuth2 保護。
如今咱們就要編寫咱們本身的受權服務。
/credentials 用於頒發客戶端憑據 (客戶端 ID 和客戶端密鑰)
/token 使用客戶端憑據頒發令牌
咱們須要實現這兩個路由。
這裏是初步的設置
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))
}
複製代碼
這裏咱們建立了一個管理器,用於客戶端存儲和鑑權服務自己。
這裏是 /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})
})
複製代碼
它建立了兩個隨機字符串,一個就是客戶端 ID,另外一個就是客戶端密鑰。並把它們保存到客戶端存儲。而後就會返回響應。就是這樣。在這裏咱們使用了內存存儲,但咱們一樣能夠把它們存儲到 redis,mongodb,postgres 等等裏面。
這裏是 /token 路由的實現:
http.HandleFunc("/token", func(w http.ResponseWriter, r *http.Request) {
srv.HandleTokenRequest(w, r)
})
複製代碼
這很是簡單。它將請求和響應傳遞給適當的處理程序,以便服務器能夠解碼請求中的全部必要的數據。
因此如下就是咱們的總體代碼:
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))
}
複製代碼
運行這個代碼併到 http://localhost:9096/credentials 路由去註冊並獲取客戶端 ID 和客戶端密鑰。
你能夠獲得具備過時時間和一些其餘信息的受權令牌。
如今咱們獲得了咱們的受權令牌。可是咱們的 /protected 路由依然沒有被保護。咱們須要設置一個方法來檢查每一個客戶端的請求是否都帶有有效的令牌。若是是的,咱們就能夠給予這個客戶端受權。反之就不能給予受權。
咱們能夠經過一箇中間件來作到這一點。
若是你知道你在作什麼,那麼在 golang 中編寫中間件會頗有趣。如下就是中間件的代碼:
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)
})
}
複製代碼
這裏將檢查請求是否帶有有效的令牌並採起對應的措施。
如今咱們須要使用 適配器/裝飾者 模式來將中間件放在咱們的 /protected 路由前面。
http.HandleFunc("/protected", validateToken(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, I'm protected"))
}, srv))
複製代碼
如今整個代碼看起來像這樣子:
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)
})
}
複製代碼
如今運行服務並在 URL 不帶有 訪問令牌 的狀況下訪問 /protected 接口。或者嘗試使用錯誤的 訪問令牌。在這兩種方式下鑑權服務都會阻止你。
如今再次從服務器得到認證信息 and 訪問令牌 併發送請求到受保護的接口:
http://localhost:9096/test?access_token=YOUR_ACCESS_TOKEN
對啦!你如今有權限訪問啦。
如今咱們已經學會了怎麼使用 Go 來設置咱們本身的 OAuth2 服務器。
在下一部分中。咱們會在 Go 中構建咱們本身的 OAuth2 客戶端。而且在最後一部分,咱們會基於登陸和受權構建咱們本身的 基於服務器的受權碼模式。
若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。
掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 Android、iOS、前端、後端、區塊鏈、產品、設計、人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。