Categories
程式開發

用Go-Guardian寫一個Golang的可擴展的身份認證


作者: Sanad Haj 譯者:朱亞光策劃:TinaSanad Haj:就職於F5Networks的軟件工程師原文鏈接 使用Go-Guardian在Golang中編寫可擴展身份驗證

在構建web和REST API 應用中,如何打造一個用戶信任和依賴的系統是非常重要的。

身份認證很重要,因為它通過只允許經過身份認證的用戶訪問其受保護的資源,從而使得機構和應用程序能夠來保持其網絡的安全。

在本教程中,我們將討論如何使用Golang和Go-Guardian庫來處理運行在集群模式下程序的身份驗證。

問題

只要用戶信息存儲或者緩存在服務器端,身份認證就是一個可能會導致擴展性問題的地方。

在Kubernetes、docker swarm等集群模式下,甚至在LB後端,運行無狀態的應用程序,都不能保證將單個服務器分配給特定的用戶。

用例和解決方案

假設我沒有兩個可複制的應用程序A和B,並且運行在LB後面。 當用戶通過LB路由向應用程序A請求token,這個時候token已經產生並且緩存在應用程序中,同時同一個用戶通過LB路由向應用程序B請求受保護的資源,這個會導致身份認證錯誤而請求失敗。

讓我們想想如何解決上述問題,在不降低性能的情況下擴展應用程序,並記住這種服務必須是無狀態的。

建議解決方法:

token存儲在db中,服務器中程序緩存。 分佈式緩存共享緩存粘性會話

上面所有的方法都會面臨同一個問題,我們試想一下,如果數據庫或者共享緩存掛了,甚至程序本身掛了會發生什麼?

解決這類問題的最佳解決方案就是使用無狀態token,在該token裡面可以再次對其進行簽名和驗證。

在本教程中,我們將使用RFC 7519中定義的JWT,主要是因為其在網絡上大家使用的比較廣泛,都使用過是聽說過。

守護者概述

Go-Guardian 是一個golang庫,它提供了一種簡單、簡潔和慣用的方法來構造強大先進的API和web身份驗證。

Go-Guardian的唯一目的就是驗證請求,他通過一組被稱為策略的可擴展的身份認證方法來實現。 Go-Guardian不掛載路由也不假設任何特定的數據庫模式,這極大提高了靈活性,允許開發者自己做決定。

API很簡單:你提交請求給Go-Guardian進行身份驗證,Go-Guardian調用策略來進行最終用戶的請求認證。 策略提供回調方法來控制當身份認證成功或者失敗的情況。

為什麼要使用Go-Guardian

當構建一個現代應用程序時,你肯定不希望重複造輪子。 而且當你聚焦精力構建一個優秀的軟件時,Go-Guardian正好解決了你的燃眉之急。

下面有幾個可以讓你嘗試一下的理由:

提供了簡單、簡介、慣用的API。 提供了最流行和最傳統的身份認證方法。 基於不同的機制和算法,提供一個包來緩存身份驗證決策。 提供了基於RFC-4226和RFC-6238的雙向身份認證和一次性密碼。

創建我們的項目

我們開始新建一個項目

mkdir可擴展身份驗證&& cd可擴展身份驗證&& go mod init可擴展身份驗證&& touch main.go

新建了一個“scalable-auth”的文件夾,並且go.mod初始化。

當然我們也需要安裝gorilla mux,、go-guardian、jwt-go“。

`go get github.com/gorilla/mux`
`go get github.com/shaj13/go-guardian`
`go get "github.com/dgrijalva/jwt-go"`

我們的第一行代碼

在我們寫任何代碼之前,我們需要寫一些強制代碼來運行程序。

package main
import (
"log"
)
func main() {
log.Println("Auth !!")
}

創建我們的endpoints

我們將刪掉打印“Auth!!”那行代碼,添加gorilla Mux包初始化路由。

package main
import (
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
}

現在我們要建立我們API的endpoints,我們把所有的endpoints都創建在main函數里面,每一個endpoint都需要一個函數來處理請求。

package main
import (
"net/http"
"log"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/v1/auth/token", createToken).Methods("GET")
router.HandleFunc("/v1/book/{id}", getBookAuthor).Methods("GET")
log.Println("server started and listening on http://127.0.0.1:8080")
http.ListenAndServe("127.0.0.1:8080", router)
}

我們創建了兩個路由,第一個是獲取token的API,第二個是獲取受保護的資源的信息,即通過id書的作者信息。

路由處理程序

現在我們只需要定義處理請求的函數了

createToken()

func createToken(w http.ResponseWriter, r *http.Request) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
"iss": "auth-app",
"sub": "medium",
"aud": "any",
"exp": time.Now().Add(time.Minute * 5).Unix(),
})
jwtToken, _:= token.SignedString([]byte("secret"))
w.Write([]byte(jwtToken))
}

getBookAuthor()

func getBookAuthor(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r)
id := vars["id"]
books := map[string]string{
"1449311601": "Ryan Boyd",
"148425094X": "Yvonne Wilson",
"1484220498": "Prabath Siriwarden",
}
body := fmt.Sprintf("Author: %s n", books[id])
w.Write([]byte(body))
}

現在我們來發送一些簡單的請求來測試下代碼!

curl -k http://127.0.0.1:8080/v1/book/1449311601
Author: Ryan Boyd

curl -k http://127.0.0.1:8080/v1/auth/token

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhbnkiLCJleHAiOjE1OTczNjE0NDYsImlzcyI6ImF1dGgtYXBwIiwic3ViIjoibWVkaXVtIn0.EepQzhuAS-lnljTZad3vAO2vRbgflB53aUCfCnlbku4

使用Go-Guardian集成

首先我們在main函數前面添加下面的變量定義

var authenticator auth.Authenticator
var cache store.Cache

接著我們寫兩個函數來驗證用戶的憑證和token

func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {
if userName == "medium" && password == "medium" {
return auth.NewDefaultUser("medium", "1", nil, nil), nil
}
return nil, fmt.Errorf("Invalid credentials")
}
func verifyToken(ctx context.Context, r *http.Request, tokenString string) (auth.Info, error) {
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
}
return []byte("secret"), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
user := auth.NewDefaultUser(claims["medium"].(string), "", nil, nil)
return user, nil
}
return nil , fmt.Errorf("Invaled token")
}

我們還需要一個函數來新建Go-Guardian.

func setupGoGuardian() {
authenticator = auth.New()
cache = store.NewFIFO(context.Background(), time.Minute*5)
basicStrategy := basic.New(validateUser, cache)
tokenStrategy := bearer.New(verifyToken, cache)
authenticator.EnableStrategy(basic.StrategyKey, basicStrategy)
authenticator.EnableStrategy(bearer.CachedStrategyKey, tokenStrategy)
}

我們構造一個authenticator來接受請求,並且將其分發給策略,並且第一個成功驗證的請求返回用戶信息。 另外初始化一塊緩存來緩存身份認證的結果能夠提高服務器性能。

接著我們需要一個HTTP的中間件來攔截請求,使得請求到達最終的路由之前進行用戶的身份驗證。

func middleware(next http.Handler) http.HandlerFunc {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Println("Executing Auth Middleware")
user, err := authenticator.Authenticate(r)
if err != nil {
code := http.StatusUnauthorized
http.Error(w, http.StatusText(code), code)
return
}
log.Printf("User %s Authenticatedn", user.UserName())
next.ServeHTTP(w, r)
})
}

最後我們把createToken和getBookAuthor函數封裝下,用中間件來請求身份驗證。

middleware(http.HandlerFunc(createToken))
middleware(http.HandlerFunc(getBookAuthor))

不要忘記在第一個main函數之前調用下GoGuardian

setupGoGuardian()

測試下我們的API

首先我們在兩個不同的shell終端裡面兩次運行程序

PORT=8080 go run main.go
PORT=9090 go run main.go

從副本A(8080端口)獲取token

curl -k http://127.0.0.1:8080/v1/auth/token -u medium:medium

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhbnkiLCJleHAiOjE1OTczNjI4NjksImlzcyI6ImF1dGgtYXBwIiwic3ViIjoibWVkaXVtIn0.SlignTJE3YD9Ecl24ygoYRu_9tVucCLop4vXWKzaRTw

從副本B(9090端口)使用token獲取書的作者

curl -k http://127.0.0.1:8080/v1/book/1449311601 -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhbnkiLCJleHAiOjE1OTczNjI4NjksImlzcyI6ImF1dGgtYXBwIiwic3ViIjoibWVkaXVtIn0.SlignTJE3YD9Ecl24ygoYRu_9tVucCLop4vXWKzaRTw"
Author: Ryan Boyd

感謝你的閱讀

希望這篇文章對你有用,至少希望能夠幫助你們熟悉使用Go-Guardian來構建一個最基本的服務端身份認證。 很多關於Go-Guardian你可以訪問的GitHub “和GoDoc