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

{"type":"doc","content":[{"type":"blockquote","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作者: Sanad Haj 譯者:朱亞光 策劃:Tina"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Sanad Haj:就職於F5Networks的軟件工程師 "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接 "},{"type":"link","attrs":{"href":"https://medium.com/@hajsanad/writing-scalable-authentication-in-golang-using-go-guardian-83691219a73a","title":""},"content":[{"type":"text","text":"Writing Scalable Authentication in Golang Using Go-Guardian"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在構建web和REST API 應用中,如何打造一個用戶信任和依賴的系統是非常重要的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"身份認證很重要,因爲它通過只允許經過身份認證的用戶訪問其受保護的資源,從而使得機構和應用程序能夠來保持其網絡的安全。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在本教程中,我們將討論如何使用Golang和Go-Guardian庫來處理運行在集羣模式下程序的身份驗證。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"問題"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"只要用戶信息存儲或者緩存在服務器端,身份認證就是一個可能會導致擴展性問題的地方。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在Kubernetes、docker swarm等集羣模式下,甚至在LB後端,運行無狀態的應用程序,都不能保證將單個服務器分配給特定的用戶。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"用例和解決方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"假設我沒有兩個可複製的應用程序A和B,並且運行在LB後面。當用戶通過LB路由嚮應用程序A請求token,這個時候token已經產生並且緩存在應用程序中,同時同一個用戶通過LB路由嚮應用程序B請求受保護的資源,這個會導致身份認證錯誤而請求失敗。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"讓我們想想如何解決上述問題,在不降低性能的情況下擴展應用程序,並記住這種服務必須是無狀態的。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"建議解決方法:"}]},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"token存儲在db中,服務器中程序緩存。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"分佈式緩存"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"共享緩存"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"粘性會話"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"上面所有的方法都會面臨同一個問題,我們試想一下,如果數據庫或者共享緩存掛了,甚至程序本身掛了會發生什麼?"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"解決這類問題的最佳解決方案就是使用無狀態token,在該token裏面可以再次對其進行簽名和驗證。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在本教程中,我們將使用RFC 7519中定義的JWT,主要是因爲其在網絡上大家使用的比較廣泛,都使用過是聽說過。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"Go-Guardian 概述"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go-Guardian 是一個golang庫,它提供了一種簡單、簡潔和慣用的方法來構造強大先進的API和web身份驗證。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Go-Guardian的唯一目的就是驗證請求,他通過一組被稱爲策略的可擴展的身份認證方法來實現。Go-Guardian不掛載路由也不假設任何特定的數據庫模式,這極大提高了靈活性,允許開發者自己做決定。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"API很簡單:你提交請求給Go-Guardian進行身份驗證,Go-Guardian調用策略來進行最終用戶的請求認證。策略提供回調方法來控制當身份認證成功或者失敗的情況。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"爲什麼要使用Go-Guardian "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當構建一個現代應用程序時,你肯定不希望重複造輪子。而且當你聚焦精力構建一個優秀的軟件時,Go-Guardian正好解決了你的燃眉之急。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"下面有幾個可以讓你嘗試一下的理由:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供了簡單、簡介、慣用的API。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供了最流行和最傳統的身份認證方法。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於不同的機制和算法,提供一個包來緩存身份驗證決策。"}]}]},{"type":"listitem","content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"提供了基於RFC-4226和RFC-6238的雙向身份認證和一次性密碼。"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"創建我們的項目"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們開始新建一個項目"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"codeinline","content":[{"type":"text","text":"mkdir scalable-auth && cd scalable-auth && go mod init scalable-auth && touch main.go"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"新建了一個“scalable-auth”的文件夾,並且go.mod初始化。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"當然我們也需要安裝gorilla mux,、go-guardian、"},{"type":"link","attrs":{"href":"https://github.com/dgrijalva/jwt-go","title":""},"content":[{"type":"text","text":"jwt-go"}]},{"type":"text","text":"。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"`go get github.com/gorilla/mux`\n`go get github.com/shaj13/go-guardian`\n`go get \"github.com/dgrijalva/jwt-go\"`"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"我們的第一行代碼"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們寫任何代碼之前,我們需要寫一些強制代碼來運行程序。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"package main\nimport (\n \"log\"\n)\nfunc main() {\n log.Println(\"Auth !!\")\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"創建我們的endpoints"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們將刪掉打印“Auth!!”那行代碼,添加gorilla Mux包初始化路由。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"package main\nimport (\n \"github.com/gorilla/mux\"\n)\nfunc main() {\n router := mux.NewRouter()\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們要建立我們API的endpoints,我們把所有的endpoints都創建在main函數裏面,每一個endpoint都需要一個函數來處理請求。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"package main\nimport (\n \"net/http\"\n \"log\"\n \"github.com/gorilla/mux\"\n)\nfunc main() {\n router := mux.NewRouter() \n router.HandleFunc(\"/v1/auth/token\", createToken).Methods(\"GET\")\n router.HandleFunc(\"/v1/book/{id}\", getBookAuthor).Methods(\"GET\")\n log.Println(\"server started and listening on http://127.0.0.1:8080\")\n http.ListenAndServe(\"127.0.0.1:8080\", router)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們創建了兩個路由,第一個是獲取token的API,第二個是獲取受保護的資源的信息,即通過id書的作者信息。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"路由處理程序"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們只需要定義處理請求的函數了"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"createToken()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func createToken(w http.ResponseWriter, r *http.Request) {\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{\n\t\t\"iss\": \"auth-app\",\n\t\t\"sub\": \"medium\",\n\t\t\"aud\": \"any\",\n\t\t\"exp\": time.Now().Add(time.Minute * 5).Unix(),\n\t})\njwtToken, _:= token.SignedString([]byte(\"secret\"))\n w.Write([]byte(jwtToken))\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"getBookAuthor()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func getBookAuthor(w http.ResponseWriter, r *http.Request) { vars := mux.Vars(r)\n id := vars[\"id\"]\n books := map[string]string{\n \"1449311601\": \"Ryan Boyd\",\n \"148425094X\": \"Yvonne Wilson\",\n \"1484220498\": \"Prabath Siriwarden\",\n }\n body := fmt.Sprintf(\"Author: %s \\n\", books[id])\n w.Write([]byte(body))\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現在我們來發送一些簡單的請求來測試下代碼!"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"shell"},"content":[{"type":"text","text":"curl -k http://127.0.0.1:8080/v1/book/1449311601 \nAuthor: Ryan Boyd\n\ncurl -k http://127.0.0.1:8080/v1/auth/token\n\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhbnkiLCJleHAiOjE1OTczNjE0NDYsImlzcyI6ImF1dGgtYXBwIiwic3ViIjoibWVkaXVtIn0.EepQzhuAS-lnljTZad3vAO2vRbgflB53aUCfCnlbku4"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"使用Go-Guardian集成"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們在main函數前面添加下面的變量定義"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"var authenticator auth.Authenticator\nvar cache store.Cache"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接着我們寫兩個函數來驗證用戶的憑證和token"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func validateUser(ctx context.Context, r *http.Request, userName, password string) (auth.Info, error) {\n if userName == \"medium\" && password == \"medium\" {\n return auth.NewDefaultUser(\"medium\", \"1\", nil, nil), nil\n }\n return nil, fmt.Errorf(\"Invalid credentials\")\n}\nfunc verifyToken(ctx context.Context, r *http.Request, tokenString string) (auth.Info, error) {\ntoken, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {\n if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {\n return nil, fmt.Errorf(\"Unexpected signing method: %v\", token.Header[\"alg\"])\n}\n return []byte(\"secret\"), nil\n})\nif err != nil {\n return nil, err\n}\nif claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {\nuser := auth.NewDefaultUser(claims[\"medium\"].(string), \"\", nil, nil)\nreturn user, nil\n}\nreturn nil , fmt.Errorf(\"Invaled token\")\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們還需要一個函數來新建Go-Guardian."}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func setupGoGuardian() { \n authenticator = auth.New()\n cache = store.NewFIFO(context.Background(), time.Minute*5)\n basicStrategy := basic.New(validateUser, cache) \n tokenStrategy := bearer.New(verifyToken, cache)\n authenticator.EnableStrategy(basic.StrategyKey, basicStrategy)\n authenticator.EnableStrategy(bearer.CachedStrategyKey, tokenStrategy)\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們構造一個authenticator來接受請求,並且將其分發給策略,並且第一個成功驗證的請求返回用戶信息。另外初始化一塊緩存來緩存身份認證的結果能夠提高服務器性能。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"接着我們需要一個HTTP的中間件來攔截請求,使得請求到達最終的路由之前進行用戶的身份驗證。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"go"},"content":[{"type":"text","text":"func middleware(next http.Handler) http.HandlerFunc {\n return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n log.Println(\"Executing Auth Middleware\")\n user, err := authenticator.Authenticate(r)\n if err != nil {\n code := http.StatusUnauthorized\n http.Error(w, http.StatusText(code), code)\n return\n }\n log.Printf(\"User %s Authenticated\\n\", user.UserName())\n next.ServeHTTP(w, r)\n })\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"最後我們把createToken和getBookAuthor函數封裝下,用中間件來請求身份驗證。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"middleware(http.HandlerFunc(createToken))\nmiddleware(http.HandlerFunc(getBookAuthor))"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"不要忘記在第一個main函數之前調用下 GoGuardian "}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"setupGoGuardian()"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"測試下我們的API"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"首先我們在兩個不同的shell終端裏面兩次運行程序"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"PORT=8080 go run main.go\nPORT=9090 go run main.go"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從副本A(8080端口)獲取token"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"curl -k http://127.0.0.1:8080/v1/auth/token -u medium:medium\n\neyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhbnkiLCJleHAiOjE1OTczNjI4NjksImlzcyI6ImF1dGgtYXBwIiwic3ViIjoibWVkaXVtIn0.SlignTJE3YD9Ecl24ygoYRu_9tVucCLop4vXWKzaRTw"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"從副本B(9090端口)使用token獲取書的作者"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":""},"content":[{"type":"text","text":"curl -k http://127.0.0.1:8080/v1/book/1449311601 -H \"Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJhbnkiLCJleHAiOjE1OTczNjI4NjksImlzcyI6ImF1dGgtYXBwIiwic3ViIjoibWVkaXVtIn0.SlignTJE3YD9Ecl24ygoYRu_9tVucCLop4vXWKzaRTw\"\nAuthor: Ryan Boyd"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"感謝你的閱讀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"希望這篇文章對你有用,至少希望能夠幫助你們熟悉使用Go-Guardian來構建一個最基本的服務端身份認證。很多關於 Go-Guardian你可以訪問"},{"type":"link","attrs":{"href":"https://github.com/shaj13/go-guardian/tree/master/auth","title":""},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"GitHub"},{"type":"text","text":" "}]},{"type":"text","text":"and"},{"type":"link","attrs":{"href":"https://pkg.go.dev/github.com/shaj13/go-guardian?tab=doc","title":null},"content":[{"type":"text","marks":[{"type":"italic"}],"text":"GoDoc"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章