写点什么

用 Go-Guardian 写一个 Golang 的可扩展的身份认证

用户头像
朱亚光
关注
发布于: 2020 年 09 月 15 日
用Go-Guardian写一个Golang的可扩展的身份认证

作者: Sanad Haj 译者:朱亚光 策划:Tina

Sanad Haj:就职于 F5Networks 的软件工程师

原文链接 Writing Scalable Authentication in Golang Using Go-Guardian


在构建 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 概述

Go-Guardian 是一个 golang 库,它提供了一种简单、简洁和惯用的方法来构造强大先进的 API 和 web 身份验证。


Go-Guardian 的唯一目的就是验证请求,他通过一组被称为策略的可扩展的身份认证方法来实现。Go-Guardian 不挂载路由也不假设任何特定的数据库模式,这极大提高了灵活性,允许开发者自己做决定。

API 很简单:你提交请求给 Go-Guardian 进行身份验证,Go-Guardian 调用策略来进行最终用户的请求认证。策略提供回调方法来控制当身份认证成功或者失败的情况。


为什么要使用 Go-Guardian

当构建一个现代应用程序时,你肯定不希望重复造轮子。而且当你聚焦精力构建一个优秀的软件时,Go-Guardian 正好解决了你的燃眉之急。


下面有几个可以让你尝试一下的理由:


  • 提供了简单、简介、惯用的 API。

  • 提供了最流行和最传统的身份认证方法。

  • 基于不同的机制和算法,提供一个包来缓存身份验证决策。

  • 提供了基于 RFC-4226 和 RFC-6238 的双向身份认证和一次性密码。


创建我们的项目


我们开始新建一个项目


mkdir scalable-auth && cd scalable-auth && go mod init scalable-auth && 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 mainimport (  "log")func main() {  log.Println("Auth !!")}
复制代码


创建我们的 endpoints

我们将删掉打印“Auth!!”那行代码,添加 gorilla Mux 包初始化路由。


package mainimport (  "github.com/gorilla/mux")func main() {  router := mux.NewRouter()}
复制代码


现在我们要建立我们 API 的 endpoints,我们把所有的 endpoints 都创建在 main 函数里面,每一个 endpoint 都需要一个函数来处理请求。


package mainimport (  "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.Authenticatorvar 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 Authenticated\n", 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.goPORT=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 andGoDoc

用户头像

朱亚光

关注

你说我们是不是完了 2018.01.27 加入

容器热 kubernetes热 微服务热 DevOps热 =====》云原生热 我也很热

评论

发布
暂无评论
用Go-Guardian写一个Golang的可扩展的身份认证