用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"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章