- 原文地址: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,這都會是很有幫助的。所以現在讓我們開始吧。
什麼是 OAuth2?
開放授權版本 2.0 被稱為 OAuth2。它是一種保護 RESTful Web 服務的協議或者說是框架。OAuth2 非常強大。由於 OAuth2 堅如磐石的安全性,所以現在大多數的 REST API 都通過 OAuth2 進行保護。
OAuth2 具有兩個部分
-
客戶端
-
服務端
OAuth2 客戶端
如果你熟悉這個介面,你就會知道我將要說什麼。但是無論熟悉與否,都讓我來講一下這個圖片背後的故事吧。
你正在構建一個面向使用者的應用程式,它是與使用者的 github 倉庫協同使用的。比如:就像是 TravisCI、CircleCI 和 Drone 等 CI 工具。
但是使用者的 github 賬戶是被保護的,如果所有者不願意任何人都無權訪問。那麼這些 CI 工具如何訪問使用者的 github 帳戶和倉庫的呢?
這其實很簡單。
你的應用程式會詢問使用者
“為了與我們的服務協作,我們需要得到你的 github 倉庫的讀取許可權。你同意嗎?”
然後這個使用者就會說
“我同意。你們可以去做你們需要做的事兒啦。"
然後你的應用程式會請求 github 的許可權管理以獲得那個特定使用者的 github 訪問許可權。Github 會檢查是否屬實並要求該使用者進行授權。通過之後 github 就會給這個客戶端傳送一個臨時的令牌。
現在,當你的應用程式得到身份驗證和授權以後需要訪問 github 時,就需要把這個令牌在請求中間帶過去,github 收到了之後就會想:
“咦,這個訪問令牌看起來很眼熟嘛,應該是我們之前就給過你了。好,你可以訪問了”
這是一個很長的流程。但是時代已經變啦,現在你不用每次都去 github 授權中心(當然我們從來也不需要這樣)。每件事都可以自動化地完成。
但是怎麼完成呢?
這是我前幾分鐘討論的內容所對應的 UML 時序圖。就是一個對應的圖形表示。
從上圖中,我們可以發現幾點重要的東西。
OAuth2 有 4 個角色:
-
使用者 — 最終使用你的應用程式的使用者
-
客戶端 — 就是你構建的那個會使用 github 賬戶的應用程式,也就是使用者會使用的東西
-
鑑權伺服器 — 這個伺服器主要處理 OAuth 相關事務
-
資源伺服器 — 這個伺服器有那些被保護的資源。比如說 github
客戶端代表使用者向鑑權伺服器傳送 OAuth2 請求。
構建一個 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、前端、後端、區塊鏈、產品、設計、人工智慧等領域,想要檢視更多優質譯文請持續關注 掘金翻譯計劃、官方微博、知乎專欄。