Golang基于JWT与Casbin身份验证授权实例详解

2022-08-07 12:52:18
目录
JWTHeaderPayloadSignatureJWT的优势JWT的使用场景CasbinCasbin可以做什么Casbin不可以做什么Casbin的工作原理实践登录接口请求Token实现使用Redis存储Token信息用Casbin做授权管理实现Casbin的策略创建Todo

JWT

JSON>

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

下图是通过JWT.io解码,查看JWT token的组成

可以看出JWT是由下面三个部分组成的:

    头部(Header)载荷(Payload)签名(Signature)

    Header

    Header是Token的构成的第一部分,包含了Token类型、Token使用的加密算法(加密算法可以是HMAC,>

    Payload

    Payload是token的第二部分,由JWT标准中注册的、公共的、私有的声明三部分组成。Payload通常包含一些用户的声明信息,比如签发者、过期时间、签发时间等。其中最常见的是issuer,>

      issuer被用来标识token的颁发人expiration是token的过期时间subject被用来标识token主体部分

      Signature

      Signature是由头部和载荷加密后连接起来的,程序通过验证Signature是否合法来决定认证是否通过

      JWT的优势

        体积小。JWT是采用JSON进行通信的,JSON比XML更加简洁,因此在对其编码时,JWT的体积比SAML更小(SAML是一种基于XML的开放标准,用在身份提供者和服务提供者之间交换身份验证和授权的数据,SAML的一个重要的应用就是基于Web的单点登录)更加安全。JWT能够使用公钥或者私钥对证书进行加密或解密,虽然SAML也可以使用JWT等公钥或私钥进行加密或解密,但是与JSON相比,使用XML数字签名容易引进比较晦涩的安全漏洞更加通用。JSON可以转换成很多语言的对象方式,而XML没有一种可以转为对象的映射更容易处理。不管是在PC端还是在移动端,JSON都能够很好的进行通信

        JWT的使用场景

          身份验证。授权信息交换

          需要注意的是不要将敏感信息存在Token里面!!!

          Casbin

          Casbin是一个强大的、高效的、开源的权限访问控制库,它提供了多种权限控制访问模型,比如ACL(权限控制列表)、RBAC(基于角色的访问控制)、ABAC(基于属性的权限验证)等。除此之外Casbin还支持多种编程语言

          Casbin可以做什么

            通过经典的{subject,>或者自定义的模式执行想要的策略,同时支持allow和deny两种授权方式处理控制访问存储和权限管理用户-角色-资源权限控制访问映射(RBAC)支持超级管理员授权方式可以使用内置的函数配置访问规则

            Casbin不可以做什么

              使用用户名或密码登录的身份验证管理用户或者角色列表,这些由系统本身管理更加方便,casbin主要是用来作为用户-角色的一种权限访问控制映射

              Casbin的工作原理

              在Casbin中,访问控制模型被抽象为PERM(Policy,>的一个文件

                Request

                定义请求参数。基本请求时一个元组对象,至少需要主题(访问实体), 对象(访问资源), 动作(访问方式),例如r={sub, obj, act},它实际定义了我们应该提供访问控制匹配功能的参数名称和顺序

                  Policy

                  定义访问策略模式,例如p={sub, obj, act}或p={sub, obj, act, eff}, 它定义字段的名称和顺序

                    Matcher

                    匹配请求和策略的规则,例如m = r.sub == p.sub && r.obj == p.obj && r.act == p.act,它的意思是如果请求的参数被匹配,那么结果就会被返回

                      Effect

                      匹配后的结果会储存于Effect当中,可以对匹配结果再次做出逻辑判断,例如e = some (where (p.eft == allow))

                      Casbin中最基本的model是ACL,下面是ACL的model配置

                      [request_definition]
                      r = sub, obj, act
                      # Policy definition
                      [policy_definition] 
                      p = sub, obj, act
                      # Policy effect 
                      [policy_effect] 
                      e = some(where (p.eft == allow))
                      # Matchers 
                      [matchers]
                      m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
                      

                      实践

                      编写一个简单的TODO>

                      创建一个simple-jwt-auth的目录,然后通过go mod管理依赖 go mod init simple-jwt-auth 建立的目录结构如下:

                      在model定义UserTodo的结构体

                      // models/model.go
                      type User struct {
                         ID       string `json:"id"`
                         UserName string `json:"username"`
                         Password string `json:"password"`
                      }
                      type Todo struct {
                         UserID string `json:"user_id"`
                         Title  string `json:"title"`
                         Body   string `json:"body"`
                      }
                      // SetPassword sets a new password stored as hash.
                      func (m *User) SetPassword(password string) error {
                         if len(password) < 6 {
                            return fmt.Errorf("new password for %s must be at least 6 characters", m.UserName)
                         }
                         m.Password = password
                         return nil
                      }
                      // InvalidPassword returns true if the given password does not match the hash.
                      func (m *User) InvalidPassword(password string) bool {
                         if password == "" {
                            return true
                         }
                         if m.Password != password {
                            return true
                         }
                         return false
                      }
                      

                      登录接口请求

                      当用户通过用户名和密码等信息登录系统服务时,需要验证是否已注册、密码是否正确等,然后返回信息,>api层实现Login的接口:

                      // api/auth_api.go
                      func Login(c *gin.Context) {
                         var u models.User
                         if err := c.ShouldBindJSON(&u); err != nil {
                            c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
                            return
                         }
                         //find user with username
                         user, err := models.UserRepo.FindByID(1)
                         //compare the user from the request, with the one we defined:
                         if user.UserName != u.UserName || user.Password != u.Password {
                            c.JSON(http.StatusUnauthorized, "Please provide valid login details")
                            return
                         }
                         c.JSON(http.StatusOK, "Login successfully")
                      }
                      func Logout(c *gin.Context) {
                         c.JSON(http.StatusOK, "Successfully logged out")
                      }
                      

                      在真实的项目中,数据都是存在数据库中。在该教程中,为了方便,创建一个mock文件user_repository.go

                      // models/user_repository.go
                      var us = []User{
                         {
                            ID:       "2",
                            UserName: "users",
                            Password: "pass",
                         }, {
                            ID:       "3",
                            UserName: "username",
                            Password: "password",
                         },
                      }
                      var UserRepo = UserRepository{
                         Users: us,
                      }
                      type UserRepository struct {
                         Users []User
                      }
                      func (r *UserRepository) FindAll() ([]User, error) {
                         return r.Users, nil
                      }
                      func (r *UserRepository) FindByID(id int) (User, error) {
                         for _, v := range r.Users {
                            uid, err := strconv.Atoi(v.ID)
                            if err != nil {
                               return User{}, err
                            }
                            if uid == int(id) {
                               return v, nil
                            }
                         }
                         return User{}, errors.New("Not found")
                      }
                      func (r *UserRepository) Save(user User) (User, error) {
                         r.Users = append(r.Users, user)
                         return user, nil
                      }
                      func (r *UserRepository) Delete(user User) {
                         id := -1
                         for i, v := range r.Users {
                            if v.ID == user.ID {
                               id = i
                               break
                            }
                         }
                         if id == -1 {
                            log.Fatal("Not found user ")
                            return
                         }
                         r.Users[id] = r.Users[len(r.Users)-1] // Copy last element to index i.
                         r.Users[len(r.Users)-1] = User{}      // Erase last element (write zero value).
                         r.Users = r.Users[:len(r.Users)-1]    // Truncate slice.
                         return
                      }
                      

                      为了不让Login函数变得臃肿,生成token的逻辑放在auth目录中, 下面实现token验证逻辑

                      Token实现

                      JWT实现的系统中,用户登录后,系统会生成并返回一个token给用户,下次请求时将会带上该token进行身份验证。token有以下问题需要处理:

                        用户退出登录的时候,需要使token失效token有可能被黑客劫持和使用token过期后需要用户重新登录,体验不友好

                        上面的问题可以通过以下两种方式解决:

                          使用Redis存储token的信息。当用户退出时,使token失效,>在token过期的时候,使用刷新token的方式重新生成一个token, 不用用户退出登录,提高用户体验

                          使用Redis存储Token信息

                          使用uuid作为redis中的key,>TokenManager结构体,通过接口的方式实现token

                          type TokenManager struct{}
                          func NewTokenService() *TokenManager {
                             return &TokenManager{}
                          }
                          type TokenInterface interface {
                             CreateToken(userId, userName string) (*TokenDetails, error)
                             ExtractTokenMetadata(*http.Request) (*AccessDetails, error)
                          }
                          //Token implements the TokenInterface
                          var _ TokenInterface = &TokenManager{}
                          func (t *TokenManager) CreateToken(userId, userName string) (*TokenDetails, error) {
                             td := &TokenDetails{}
                             td.AtExpires = time.Now().Add(time.Minute * 30).Unix() //expires after 30 min
                             td.TokenUuid = uuid.NewV4().String()
                             td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix()
                             td.RefreshUuid = td.TokenUuid + "++" + userId
                             var err error
                             //Creating Access Token
                             atClaims := jwt.MapClaims{}
                             atClaims["access_uuid"] = td.TokenUuid
                             atClaims["user_id"] = userId
                             atClaims["user_name"] = userName
                             atClaims["exp"] = td.AtExpires
                             at := jwt.NewWithClaims(jwt.SigningMethodHS256, atClaims)
                             td.AccessToken, err = at.SignedString([]byte(os.Getenv("ACCESS_SECRET")))
                             if err != nil {
                                return nil, err
                             }
                             //Creating Refresh Token
                             td.RtExpires = time.Now().Add(time.Hour * 24 * 7).Unix()
                             td.RefreshUuid = td.TokenUuid + "++" + userId
                             rtClaims := jwt.MapClaims{}
                             rtClaims["refresh_uuid"] = td.RefreshUuid
                             rtClaims["user_id"] = userId
                             rtClaims["user_name"] = userName
                             rtClaims["exp"] = td.RtExpires
                             rt := jwt.NewWithClaims(jwt.SigningMethodHS256, rtClaims)
                             td.RefreshToken, err = rt.SignedString([]byte(os.Getenv("REFRESH_SECRET")))
                             if err != nil {
                                return nil, err
                             }
                             return td, nil
                          }
                          func (t *TokenManager) ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
                             token, err := VerifyToken(r)
                             if err != nil {
                                return nil, err
                             }
                             acc, err := Extract(token)
                             if err != nil {
                                return nil, err
                             }
                             return acc, nil
                          }
                          func TokenValid(r *http.Request) error {
                             token, err := VerifyToken(r)
                             if err != nil {
                                return err
                             }
                             if _, ok := token.Claims.(jwt.Claims); !ok && !token.Valid {
                                return err
                             }
                             return nil
                          }
                          func VerifyToken(r *http.Request) (*jwt.Token, error) {
                             tokenString := ExtractToken(r)
                             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(os.Getenv("ACCESS_SECRET")), nil
                             })
                             if err != nil {
                                return nil, err
                             }
                             return token, nil
                          }
                          //get the token from the request body
                          func ExtractToken(r *http.Request) string {
                             bearToken := r.Header.Get("Authorization")
                             strArr := strings.Split(bearToken, " ")
                             if len(strArr) == 2 {
                                return strArr[1]
                             }
                             return ""
                          }
                          func Extract(token *jwt.Token) (*AccessDetails, error) {
                             claims, ok := token.Claims.(jwt.MapClaims)
                             if ok && token.Valid {
                                accessUuid, ok := claims["access_uuid"].(string)
                                userId, userOk := claims["user_id"].(string)
                                userName, userNameOk := claims["user_name"].(string)
                                if ok == false || userOk == false || userNameOk == false {
                                   return nil, errors.New("unauthorized")
                                } else {
                                   return &AccessDetails{
                                      TokenUuid: accessUuid,
                                      UserId:    userId,
                                      UserName:  userName,
                                   }, nil
                                }
                             }
                             return nil, errors.New("something went wrong")
                          }
                          func ExtractTokenMetadata(r *http.Request) (*AccessDetails, error) {
                             token, err := VerifyToken(r)
                             if err != nil {
                                return nil, err
                             }
                             acc, err := Extract(token)
                             if err != nil {
                                return nil, err
                             }
                             return acc, nil
                          }
                          

                          上面的代码设置token的有效时间为30分钟,30分钟过后token将失效,用户不能使用该token进行正确验证。

                          另外,使用了从.env配置文件获取的密钥(ACCESS_SECRET)签名。在真实的项目,不能在代码中公开这个密钥!!!

                          REDIS_HOST=127.0.0.1
                          REDIS_PORT=6379
                          REDIS_PASSWORD=
                          ACCESS_SECRET=98hbun98hsdfsdwesdfs
                          REFRESH_SECRET=786dfdbjhsbsdfsdfsdf
                          PORT=8081
                          

                          定义AuthInterface处理会话

                          package auth
                          import (
                             "errors"
                             "fmt"
                             "github.com/go-redis/redis/v7"
                             "time"
                          )
                          type AccessDetails struct {
                             TokenUuid string
                             UserId    string
                             UserName  string
                          }
                          type TokenDetails struct {
                             AccessToken  string
                             RefreshToken string
                             TokenUuid    string
                             RefreshUuid  string
                             AtExpires    int64
                             RtExpires    int64
                          }
                          type AuthInterface interface {
                             CreateAuth(string, *TokenDetails) error
                             FetchAuth(string) (string, error)
                             DeleteRefresh(string) error
                             DeleteTokens(*AccessDetails) error
                          }
                          type RedisAuthService struct {
                             client *redis.Client
                          }
                          var _ AuthInterface = &RedisAuthService{}
                          func NewAuthService(client *redis.Client) *RedisAuthService {
                             return &RedisAuthService{client: client}
                          }
                          //Save token metadata to Redis
                          func (tk *RedisAuthService) CreateAuth(userId string, td *TokenDetails) error {
                             at := time.Unix(td.AtExpires, 0) //converting Unix to UTC(to Time object)
                             rt := time.Unix(td.RtExpires, 0)
                             now := time.Now()
                             atCreated, err := tk.client.Set(td.TokenUuid, userId, at.Sub(now)).Result()
                             if err != nil {
                                return err
                             }
                             rtCreated, err := tk.client.Set(td.RefreshUuid, userId, rt.Sub(now)).Result()
                             if err != nil {
                                return err
                             }
                             if atCreated == "0" || rtCreated == "0" {
                                return errors.New("no record inserted")
                             }
                             return nil
                          }
                          //Check the metadata saved
                          func (tk *RedisAuthService) FetchAuth(tokenUuid string) (string, error) {
                             userid, err := tk.client.Get(tokenUuid).Result()
                             if err != nil {
                                return "", err
                             }
                             return userid, nil
                          }
                          //Once a user row in the token table
                          func (tk *RedisAuthService) DeleteTokens(authD *AccessDetails) error {
                             //get the refresh uuid
                             refreshUuid := fmt.Sprintf("%s++%s", authD.TokenUuid, authD.UserId)
                             //delete access token
                             deletedAt, err := tk.client.Del(authD.TokenUuid).Result()
                             if err != nil {
                                return err
                             }
                             //delete refresh token
                             deletedRt, err := tk.client.Del(refreshUuid).Result()
                             if err != nil {
                                return err
                             }
                             //When the record is deleted, the return value is 1
                             if deletedAt != 1 || deletedRt != 1 {
                                return errors.New("something went wrong")
                             }
                             return nil
                          }
                          func (tk *RedisAuthService) DeleteRefresh(refreshUuid string) error {
                             //delete refresh token
                             deleted, err := tk.client.Del(refreshUuid).Result()
                             if err != nil || deleted == 0 {
                                return err
                             }
                             return nil
                          }
                          

                          用Casbin做授权管理

                          在Casbin中,一个权限访问控制模型的配置文件是基于PERM(Policy,>的方式,因此当要修改或升级权限的时候非常方便,只需要修改配置文件就行了。使用者可以自定义配置文件,例如定义RBAC或者ACL

                          最基本的也是最简单的模型是ACL, 下面创建一个ACL的模型配置文件

                          [request_definition]
                          r = sub, obj, act
                          [policy_definition]
                          p = sub, obj, act
                          [role_definition]
                          g = _, _
                          [policy_effect]
                          e = some(where (p.eft == allow))
                          [matchers]
                          m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act
                          

                          Casbin 的权限是存储在.csv文件中或者是SQL数据库中, 在该教程是通过csv文件的方式存储

                          p, user, resource, read
                          p, username, resource, read
                          p, admin, resource, read
                          p, admin, resource, write
                          g, alice, admin
                          g, bob, user
                          

                          上面权限的意思是:

                            所有的用户可以读数据,但是不能写所有的admin用户可以读数据,也可以写数据alice是admin用户,bob是普通用户 因此Alice有控制整个系统数据的权限,而Bob只有读的权限

                            实现Casbin的策略

                            首先,定义一个policies的中间件

                            import (
                               "fmt"
                               "github.com/casbin/casbin"
                               "github.com/casbin/casbin/persist"
                               "github.com/gin-gonic/gin"
                               "github.com/simple-jwt-auth/auth"
                               "log"
                               "net/http"
                            )
                            func TokenAuthMiddleware() gin.HandlerFunc {
                               return func(c *gin.Context) {
                                  err := auth.TokenValid(c.Request)
                                  if err != nil {
                                     c.JSON(http.StatusUnauthorized, "unauthorized")
                                     c.Abort()
                                     return
                                  }
                                  c.Next()
                               }
                            }
                            // Authorize determines if current subject has been authorized to take an action on an object.
                            func Authorize(obj string, act string, adapter persist.Adapter) gin.HandlerFunc {
                               return func(c *gin.Context) {
                                  err := auth.TokenValid(c.Request)
                                  if err != nil {
                                     c.JSON(http.StatusUnauthorized, "user hasn't logged in yet")
                                     c.Abort()
                                     return
                                  }
                                  metadata, err := auth.ExtractTokenMetadata(c.Request)
                                  if err != nil {
                                     c.JSON(http.StatusUnauthorized, "unauthorized")
                                     return
                                  }
                                  // casbin enforces policy
                                  ok, err := enforce(metadata.UserName, obj, act, adapter)
                                  //ok, err := enforce(val.(string), obj, act, adapter)
                                  if err != nil {
                                     log.Println(err)
                                     c.AbortWithStatusJSON(500, "error occurred when authorizing user")
                                     return
                                  }
                                  if !ok {
                                     c.AbortWithStatusJSON(403, "forbidden")
                                     return
                                  }
                                  c.Next()
                               }
                            }
                            func enforce(sub string, obj string, act string, adapter persist.Adapter) (bool, error) {
                               enforcer := casbin.NewEnforcer("config/rbac_model.conf", adapter)
                               err := enforcer.LoadPolicy()
                               if err != nil {
                                  return false, fmt.Errorf("failed to load policy from DB: %w", err)
                               }
                               ok := enforcer.Enforce(sub, obj, act)
                               return ok, nil
                            }
                            

                            然后,修改上面的LoginLogout接口, 增加身份验证及授权信息

                            func Login(c *gin.Context) {
                               var u models.User
                               if err := c.ShouldBindJSON(&u); err != nil {
                                  c.JSON(http.StatusUnprocessableEntity, "Invalid json provided")
                                  return
                               }
                               //find user with username
                               user, err := models.UserRepo.FindByID(1)
                               //compare the user from the request, with the one we defined:
                               if user.UserName != u.UserName || user.Password != u.Password {
                                  c.JSON(http.StatusUnauthorized, "Please provide valid login details")
                                  return
                               }
                               ts, err := tokenManager.CreateToken(user.ID, user.UserName)
                               if err != nil {
                                  c.JSON(http.StatusUnprocessableEntity, err.Error())
                                  return
                               }
                               save token to redis
                               saveErr := servers.HttpServer.RD.CreateAuth(user.ID, ts)
                               if saveErr != nil {
                                  c.JSON(http.StatusUnprocessableEntity, saveErr.Error())
                               }
                               tokens := map[string]string{
                                  "access_token":  ts.AccessToken,
                                  "refresh_token": ts.RefreshToken,
                               }
                               c.JSON(http.StatusOK, tokens)
                            }
                            func Logout(c *gin.Context) {
                               //If metadata is passed and the tokens valid, delete them from the redis store
                               metadata, _ := tokenManager.ExtractTokenMetadata(c.Request)
                               if metadata != nil {
                                  deleteErr := servers.HttpServer.RD.DeleteTokens(metadata)
                                  if deleteErr != nil {
                                     c.JSON(http.StatusBadRequest, deleteErr.Error())
                                     return
                                  }
                               }
                               c.JSON(http.StatusOK, "Successfully logged out")
                            }
                            

                            创建Todo

                            定义Todo的结构体

                            type Todo struct {
                                UserID string `json:"user_id"`
                                Title string `json:"title"`
                                Body string `json:"body"`
                            }
                            

                            创建Todo的接口

                            package api
                            import (
                               "github.com/gin-gonic/gin"
                               "github.com/simple-jwt-auth/auth"
                               "github.com/simple-jwt-auth/models"
                               "net/http"
                            )
                            func CreateTodo(c *gin.Context) {
                               var td models.Todo
                               if err := c.ShouldBindJSON(&td); err != nil {
                                  c.JSON(http.StatusUnprocessableEntity, "invalid json")
                                  return
                               }
                               metadata, err := auth.ExtractTokenMetadata(c.Request)
                               if err != nil {
                                  c.JSON(http.StatusUnauthorized, "unauthorized")
                                  return
                               }
                               td.UserID = metadata.UserId
                               //you can proceed to save the  to a database
                               c.JSON(http.StatusCreated, td)
                            }
                            func GetTodo(c *gin.Context) {
                               metadata, err := auth.ExtractTokenMetadata(c.Request)
                               if err != nil {
                                  c.JSON(http.StatusUnauthorized, "unauthorized")
                                  return
                               }
                               userId := metadata.UserId
                               c.JSON(http.StatusOK, models.Todo{
                                  UserID: userId,
                                  Title:  "Return todo",
                                  Body:   "Return todo for testing",
                               })
                            }
                            

                            注册路由

                            func (s *Server) InitializeRoutes() {
                               s.Router.POST("/login", api.Login)
                               authorized := s.Router.Group("/")
                               authorized.Use(gin.Logger())
                               authorized.Use(gin.Recovery())
                               authorized.Use(middleware.TokenAuthMiddleware())
                               {
                                  authorized.POST("/api/todo",  middleware.Authorize("resource", "write", s.FileAdapter), api.CreateTodo)
                                  authorized.GET("/api/todo", middleware.Authorize("resource", "read", s.FileAdapter), api.GetTodo)
                                  authorized.POST("/logout", api.Logout)
                                  authorized.POST("/refresh", api.Refresh)
                               }
                            }
                            

                            最后在main.go文件中调用server层的Run方法即可运行

                            package main
                            import (
                               "github.com/joho/godotenv"
                               "github.com/simple-jwt-auth/servers"
                               "log"
                            )
                            func init() {
                               if err := godotenv.Load(); err != nil {
                                  log.Print("No .env file found")
                               }
                            }
                            func main() {
                               servers.Run()
                               log.Println("Server exiting")
                            }
                            

                            结果如下:

                            原作者仓库地址github

                            以上就是Golang基于JWT与Casbin身份验证授权实例详解的详细内容,更多关于Go JWT Casbin身份验证授权的资料请关注易采站长站其它相关文章!