Hertz中间件 | 青训营笔记

841 阅读5分钟

这是我参与「 第五届青训营 」伴学笔记创作活动的第 8 天 文章将介绍Hertz跨域配置Cors,权限校验jwt,异常处理recovery常用的中间件的使用。

主要内容

  • CORS
  • Recovery
  • jwt

配置CORS

跨源资源共享(CORS)机制允许服务器标识除了它自己的其它 origin,使得浏览器可以访问加载这些资源; 该机制也用来检查服务器是否允许浏览器发送真实的请求,通过浏览器发送"预检"请求实现,在预检请求头部中有 HTTP 方法和真实请求会用到的头。

hertz 提供 cors 跨域中间件的实现 ,这里的实现参考了 gin 的 cors

安装

go get github.com/hertz-contrib/cors

配置

biz/router下创建公共中间件文件夹

// biz/router/middleware/cors.go

func CorsMw() app.HandlerFunc {
	return cors.New(cors.Config{
		// 允许跨源访问的 origin 列表
		AllowOrigins: []string{"*"},
		// 允许客户端跨源访问所使用的 HTTP 方法列表
		AllowMethods: []string{"POST", "GET", "PUT", "DELETE", "OPTIONS"},
		// 允许使用的头信息字段列表
		AllowHeaders: []string{"Authorization, Content-Length, X-CSRF-Token, Token,session,X_Requested_With,Accept, Origin, Host, Connection, Accept-Encoding, Accept-Language,DNT, X-CustomHeader, Keep-Alive, User-Agent, X-Requested-With, If-Modified-Since, Cache-Control, Content-Type, Pragma"},
		// 允许暴露给客户端的响应头列表
		ExposeHeaders: []string{"Content-Length, Access-Control-Allow-Origin, Access-Control-Allow-Headers,Cache-Control,Content-Language,Content-Type,Expires,Last-Modified,Pragma,FooBar"},
		// 允许客户端请求携带用户凭证
		AllowCredentials: true,
		MaxAge:           12 * time.Hour,
	})
}

更多配置

main.go中引用cors中间件

h.Use(middleware.CorsMw())

异常处理

Recovery中间件是Hertz框架预置的中间件,使用 server.Default() 可以默认注册该中间件,为Hertz框架提供panic恢复的功能。

如果你不使用server.Default(),你也可以通过以下方式注册Recovery中间件:

h := server.New()
h.Use(recovery.Recovery())

Recovery中间件会恢复Hertz框架运行中的任何panic,在panic发生之后,Recover中间件会默认打印出panic的时间、内容和堆栈信息,同时通过*app.RequestContext将返回响应的状态码设置成500。

封装异常结果类

// base/result/result.go

type GlobalError struct {
   Id     string `json:"id"`
   Code   int    `json:"code"`
   Detail string `json:"detail"`
   Status string `json:"status"`
}

type IError struct {
   Code   int    `json:"code"`
   Detail string `json:"detail"`
}

func NewIError(code int, detail string) error {
   jsonBytes, _ := json.Marshal(map[string]interface{}{
      "Code":   code,
      "Detail": detail,
   })
   return errors.New(string(jsonBytes))
}

func NewError(code int, detail string) GlobalError {
   err := GlobalError{
      Code:   code,
      Detail: detail,
   }
   return err
}

Recovery配置

Recovery中间件提供了默认的panic处理函数defaultRecoveryHandler()。同时你也可以通过WithRecoveryHandler()函数来自定义出现panic后的处理函数

// biz/handler/myHandler/recovery.go

func Handler(c context.Context, ctx *app.RequestContext, err interface{}, stack []byte) {
    // 日志打印包 代码请见Github
   logging.Info(c, "[Recovery] myRecovery =", err, string(stack), string(ctx.Request.Header.UserAgent()))
   errStr := make(map[string]interface{}, 3)
   switch err.(type) {
   case result.GlobalError:
      globalError := err.(result.GlobalError)
      errStr = map[string]interface{}{
         "code":   globalError.Code,
         "detail": globalError.Detail,
         "data":   nil,
      }
   case error:
      var ie result.IError
      iError := err.(error)
      _ = json.Unmarshal([]byte(iError.Error()), &ie)
      errStr = map[string]interface{}{
         "code":   ie.Code,
         "detail": ie.Detail,
         "data":   nil,
      }
   }
   ctx.JSON(500, errStr)
   ctx.AbortWithStatus(consts.StatusInternalServerError)
}

main.go中引入

h.Use(recovery.Recovery(recovery.WithRecoveryHandler(myHandler.Handler)))

用户认证

JSON Web Token(JWT)是一个轻量级的认证规范,这个规范允许我们使用 JWT 在用户和服务器之间传递安全可靠的信息。其本质是一个 token ,是一种紧凑的 URL 安全方法,用于在网络通信的双方之间传递。 Hertz 也提供了 jwt 的实现 ,参考了 gin 的实现 。

安装

go get github.com/hertz-contrib/jwt

配置

// biz/router/middleware/jwt.go

// 定义用户登陆信息结构体
type Claim struct {
   ID       int
   Username string
}

func JwtMwInit() {
   // the jwt middleware
   authMiddleware, err := jwt.New(&jwt.HertzJWTMiddleware{
      // 置所属领域名称
      Realm: "hertz jwt",
      // 用于设置签名密钥
      Key: []byte("hertz-mylist&&sinrewxljntm"),
      // 设置 token 过期时间
      Timeout: time.Hour * 8,
      // 设置最大 token 刷新时间
      MaxRefresh: time.Hour * 4,
      // 设置 token 的获取源
      TokenLookup: "header: Authorization",
      // 设置从 header 中获取 token 时的前缀
      TokenHeadName: "Bearer",
      // 用于设置检索身份的键
      IdentityKey: IdentityKey,
      // 登陆成功后为向 token 中添加自定义负载信息
      PayloadFunc: func(data interface{}) jwt.MapClaims {
         if v, ok := data.(Claim); ok {
            return jwt.MapClaims{
               IdentityKey: v,
            }
         }
         return jwt.MapClaims{}
      },
      // 从 token 提取用户信息
      IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
         claims := jwt.ExtractClaims(ctx, c)
         claim := claims[IdentityKey].(map[string]interface{})
         return &Claim{
            ID:       int(claim["ID"].(float64)),
            Username: claim["Username"].(string),
         }
      },
      // 认证
      Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
         var loginUser user.User
         if err := c.BindAndValidate(&loginUser); err != nil {
            return "", result.NewIError(10000, "数据绑定错误")
         }
         password := loginUser.Password
         if err := gorm.DB.Where("username = ?", loginUser.Username).First(&loginUser).Error; err != nil {
            return nil, result.NewIError(10001, "用户名不存在")
         }
         if check := loginUser.CheckPassword(password); check {
            return Claim{
               ID:       loginUser.ID,
               Username: loginUser.Username,
            }, nil
         } else {
            return nil, result.NewIError(10002, "密码错误")
         }
      },
      // 鉴权
      Authorizator: func(data interface{}, ctx context.Context, c *app.RequestContext) bool {
          // 单一角色 不设权限校验
          return true
      },
      // 登录响应
      LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
         c.JSON(http.StatusOK, utils.H{
            "code":   code,
            "token":  token,
            "detail": "登陆成功",
         })
      },
      // 设置 jwt 校验流程发生错误时响应所包含的错误信息
      HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
         hlog.CtxErrorf(ctx, "jwt biz err = %+v", e.Error())
         return e.Error()
      },
      // 无权限
      Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
         c.JSON(http.StatusOK, utils.H{
            "code":   10011,
            "detail": "无权限或用户认证已过期",
            "data":   nil,
         })
      },
   })
   if err != nil {
      log.Fatal("JWT Error:" + err.Error())
   }

   HzJwtMw = authMiddleware
}

main.go中初始化jwt配置

middleware.JwtMwInit()

在需要鉴权的路由上引入

func _taskMw() []app.HandlerFunc {
    // your code...
    return []app.HandlerFunc{middleware.HzJwtMw.MiddlewareFunc()}
}

认证流程

image.png

  1. 配合 HertzJWTMiddleware.LoginHandler 使用,用户登陆调用接口进入Authenticator校验用户名密码是否正确
Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
    // todo 校验用户名密码
    
    // todo 用户名密码正确返回
    return Claim, nil
    
    // todo 用户名密码错误返回
    return nil, err
},
  1. 登陆成功进入PayloadFunc,向 token 中添加自定义负载信息
PayloadFunc: func(data interface{}) jwt.MapClaims {
    // todo 返回自定义负载信息内容
    return jwt.MapClaims{}
},
  1. 生成jwt后进入LoginResponse返回登陆信息
LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
    // todo 返回信息
},
  1. 登陆失败进入HTTPStatusMessageFunc设置失败返回信息
Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
    // todo 返回信息
},

鉴权流程

image.png

  1. 校验jwt通过后进入 IdentityHandlertoken 中提取用户负载信息
IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
    // todo 返回用户负载信息
    return Claim
},
  1. Authorizator 校验权限,判断是否有访问路由的权限
 Authorizator: func(data interface{}, ctx context.Context, c *app.RequestContext) bool {
     // todo 设置鉴权方式
     
     // todo 鉴权成功返回
     return true
     
     // todo 鉴权失败返回
     return false
 },
  1. Unauthorized 鉴权失败,返回信息
Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
   // todo 返回信息
},

源码

Hanabi-wxl/hertz-mw-example (github.com)

参考文章

hertz文档
字节开源WEB框架Hertz太香啦!