搬砖过程中做了两个通用性较强的工具

总述

在搬砖过程中遇到了如下两个问题:

  1. 身份认证模块因项目结构体不同可复用性不高。逻辑是简单的生成和解析 JWT, 但是在不同的项目中有独自的用户结构体,导致每次需要复写大量的代码去实现相同功能但是解析出来的用户信息不同。
  2. 分页字段、工具不可复用。带有分页字段的API 肯定是每个项目中都会有的问题,简答的来说每个分页API 都需要携带一些相同分页字段。在团队之间没有协商得当,多个项目之间还可能存在一边接口请求字段是 page,而另一端相同功能的字段却是 num

本着造一个轮子解决大部分通用性的问题,做成高可用的工具的初心,针对以上两个我发现的问题做了下面这两个工具,希望可以帮助到更多有相同情景的朋友们提高效率。

当然,我更知道当前这个阶段这两个工具还有很多考虑不周甚至 bug 的地方,希望可以集思广益,完善这些工具。

auth-go

一款简单易用的可以为任意结构体生成认证 JWT 并解析 Token 为目标结构体的中间件。

先看例子

生成 token

// 任意结构体的实例
user := User{
  ID:     "id code", 
  Name:   "user name", 
  Avatar: "avatar url",
}

user := OtherUser{
  MyID: 1,
  FirstName: "Alex",
  LastName: "Kung",
}

// 将任意实例带入 NewSession() 直接生成 JWT
token, err = auth.NewSession(user).GenerateAccessToken()

中间件集成了解析 token,并将解析后的数据保存至 gin.Context 中,供其他后置位的 Handler 可以随时拿到用户登录信息。

下面是我封装的中间件源码,实现的正是上面我所说的。

// Identify is a middleware that checks for a valid token
// s is the Session implementation with u what you need
// opts is the option functions for you create your advanced handler logic.
// note: get token from cookie and key is "access_token"
func Identify(s Session, opts ...OptionFunc) gin.HandlerFunc {
  return func(ctx *gin.Context) {
    tokenString, err := s.GetToken(ctx)
    if err != nil {
      ctx.Errors = append(ctx.Errors, NewGinPrivateError(err))
      ctx.JSON(http.StatusUnauthorized, invalidTokenMsg)
      ctx.Abort()
      return
    }

    if err = s.ParseToken(ctx, tokenString); err != nil {
      ctx.Errors = append(ctx.Errors, NewGinPrivateError(err))
      ctx.JSON(http.StatusUnauthorized, invalidTokenMsg)
      ctx.Abort()
      return
    }
    if err = s.SetExtIntoGinContext(ctx); err != nil {
      ctx.Errors = append(ctx.Errors, NewGinPrivateError(err))
      ctx.JSON(http.StatusUnauthorized, invalidTokenMsg)
      ctx.Abort()
      return
    }

    // custom functions before next
    for _, f := range opts {
      f(ctx, s)
    }
    ctx.Next()
    return
  }
}

那么该怎么使用这个中间件呢,很简单,就是在路由注册的时候添加上这个中间件,并指名需要被解析的信息到任意你自己的结构体中。(当然得对应生成 token 时使用的那个结构体,不然没有响应字段自然没有办法被解析)

    // 使用认证中间件
    // NewSession 函数中传入生成 Token 的结构体实例或指针皆可
    // 解析到的用户信息会存在 ctx *gin.Context 中
    r.Use(auth.Identify(auth.NewSession(User{})))

    // 使用认证中间件
    r.GET("/", func(c *gin.Context) {
        data, err := auth.GetExtFromGinContext(c)
        if err != nil {
            c.AbortWithStatus(401)
            return
        }
    // 使用自己定义的用户信息断言
        user := data.(User)
        c.JSON(200, gin.H{
            "user": user,
        })

    })

最佳实践应该是从 gin.Context 中取到想要的数据然后对其断言整个流程进行一个封装,就做到了真的一劳永逸。

func GetAuthenticationData(ctx *gin.Context) (*User, error) {
    d, err := auth.GetAuthenticationDataFrom(ctx)
    if err != nil {
        return nil, errors.Wrap(err, "get authentication data failed")
    }
    a, ok := d.(User)
    if !ok {
        return nil, errors.Wrap(err, "get authentication data failed")
    }
    return &a, nil
}

func main() {
  ...
    r.GET("/", func(c *gin.Context) {
        user, err := GetAuthenticationData(c)
        if err != nil {
            c.AbortWithStatus(401)
            return
        }
        c.JSON(200, gin.H{
            "user": user,
        })
    }
        ...

pagination-go

是统一方便的分页请求和响应处理工具。 用于解析带有分页信息的请求结构体和填充带有分页信息的响应结构体。

直接看例子,一看就会。

import "github.com/uptutu/pagination"

// 自定义请求结构体
type ListSubscribeEntitiesRequest {
   PageNum      int64
   PageSize     int64
   OrderBy      string
   IsDescending bool
   Query        string
   MyData       interface{}
}
// 还能是这样的
type ListSubscribeEntitiesRequest struct {
  Page struct {
    PageNum      int64
    PageSize     int64
    OrderBy      string
    IsDescending bool
    Query        string
  }
  MyData interface{}
}

type ListSubscribeEntitiesResponse {
   Total    int64
   PageNum  int64
   LastPage int64
   PageSize int64
   // 自己的数据
   Data     string
}

func (s *SubscribeService) ListSubscribeEntities(ctx context.Context, req *pb.ListSubscribeEntitiesRequest) (*pb.ListSubscribeEntitiesResponse, error) {
   page, err := pagination.Parse(req)

   //输出类似于 {Num:10 Size:50 OrderBy:test IsDescending:true SearchKey:"search"}
   fmt.Println(page)

   // returned Bool 判读是否需要分页
   // return page_num > 0 && page_size > 0 
   page.Required()

   // 返回限制个数(page_size)
   page.Limit()

   // 返回分页请求下标(page_num-1 * page_size)
   page.Offset()

   // 是否倒序
   page.IsDescending

   // 搜索关键字
   page.Query

   // 排序字段
   page.OrderBy

   // 获取总数
   count := db.Find(data).Count()

   // 请务必手动填充总数
   page.SetTotal(count)

   resp := &ListSubscribeEntitiesResponse{}
   resp.Data = "一些数据"

   // 此后 resp 的分页信息将会自动被填充
   page.FillResponse(resp)
}

总结

两个工具都大量的使用了反射(reflect)技术来实现取值与赋值,因为设计的初衷是可用自由的传任意符合要求的结构体进来,所以函数入参为 interface{} 类型(go 1.18 开始可用叫 any 了) ,写的时候碰了蛮多的坑,下一次有机会我再分享一下怎么玩反射,那就先这样。

欢迎大家 star 和 issue

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 2

你这源码是基于gin框架,如果不用gin呢?是否参数不应该传c,应该传w,r,去掉框架限制(每个框架都会存在w,r)。
分页数据一般是在返回时才需要封装,为何不写个工具类 utiles.Pagination(data interface{},total,pageNum,pageSize int)

1年前 评论
uptutu (楼主) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!