Hertz JWT 中间件 | 青训营笔记

342 阅读3分钟

这是我参与「第五届青训营 」伴学笔记创作活动的第 6 天

JSON Web Token(JWT)

JWT 是一个开放标准(RFC 7519),它定义了一种紧凑的、自包含的方式,用于作为JSON对象在各方之间安全地传输信息。该信息可以被验证和信任。

JWT 的结构

Herder.Paylod.Signature

  • Header: 包含 Token 类型和 Token 使用的算法的名称的 JSON Base64 字符串

  • Payload: 包含一些自定义信息,以及一些预定义的信息(如:exp 过期时间)的 JSON Base64 字符串

  • Signature: 将 Herder.Payload 加上 secret 使用 Header 中算法得到的 HEX 字符串

可以在 JSON Web Token 可视化查看部分结构,以及各种语言的提供的 JWT 工具

Hertz 的 JWT 中间件 的使用

中间件的源码地址:hertz-contrib/jwt

登录流程:

  1. 调用 jwt.HertzJWTMiddleware 对象的 LoginHandler 方法
  2. 进入 jwt.HertzJWTMiddleware 对象的 Authenticator 方法,用来对登录请求进行处理,这里有两种方式处理
    • 直接在 Authenticator 方法中处理登录请求,在收到登录请求后,在路由中直接调用 LoginHandler 方法
    • 或者在 LoginHandler 方法调用前将 JWT 需要包含的内容内容提前设置在 app.RequestContext
  3. Authenticator 中处理会有两种情况
    • 没有返回错误:这里返回的第一个参数会传递给 PayloadFunc 方法,用来处理 JWT 的 Payload 部分需要包含哪些的自定义数据,然后内部执行完成后,会执行 LoginResponse 方法,响应登录成功的数据
    • 返回了错误则会执行 HTTPStatusMessageFunc 方法,然后通过这个方法返回对应错误的错误信息,后执行 Unauthorized,在这个方法中返回错误信息到请求的客户端

认证流程:

  1. 注册 jwt.HertzJWTMiddlewareMiddlewareFunc 方法返回的中间件到需要认证的接口
  2. 请求由中间件处理,会通过自定义的 IdentityHandler 方法回去 Payload 中带的 Identity 值,并设置在 app.RequestContext 中 Key 是定义的 IdentityKey

代码:

JwtMiddleware, _ = jwtmw.New(&jwtmw.HertzJWTMiddleware{
		Key:           []byte(consts.JwtSecretKey),
		TokenLookup:   "header: Authorization, query: token, form: token", //cookie: jwt
		TokenHeadName: "Bearer",
		TimeFunc:      time.Now,
		Timeout:       time.Hour,
		MaxRefresh:    time.Hour,
		IdentityKey:   consts.JwtIdentityKey,
		// 设置从 token 提取用户信息的函数
		IdentityHandler: func(ctx context.Context, req *app.RequestContext) interface{} {
			claims := jwtmw.ExtractClaims(ctx, req)
			return claims[consts.JwtIdentityKey]
		},
		// 设置登录时为 token 添加自定义负载信息的函数,如果不传入这个参数,则 token 的 payload 部分默认存储 token 的过期时间和创建时间
		// data 对应 Authenticator 方法返回的第一个参数
		PayloadFunc: func(data interface{}) jwtmw.MapClaims {
            // 处理 Payload
			if v, ok := data.(string); ok {
				return jwtmw.MapClaims{
					consts.JwtIdentityKey: v,
				}
			}
			return jwtmw.MapClaims{}
		},
		// 认证用户的登录信息,配合 HertzJWTMiddleware.LoginHandler 使用
		Authenticator: func(ctx context.Context, req *app.RequestContext) (interface{}, error) {
			// 处理登录,返回需要保存到 Payload 中的数据
			return "110", nil
		},
		// 设置登录的响应函数
		LoginResponse: func(ctx context.Context, req *app.RequestContext, code int, token string, expire time.Time) {
			req.JSON(http.StatusOK, utils.H{
				"token":  &token,
			})
		},
		// 设置 jwt 授权失败后的响应
		Unauthorized: func(ctx context.Context, req *app.RequestContext, code int, message string) {
			req.JSON(code, utils.H{
                "err": message
            })
		},
		// 一旦 jwt 校验流程产生错误将执行,并返回错误的 message,然后执行 Unauthorized
		HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
            // 之间将错误对应的信息返回
			return e.Error()
		},
	})

部分代码解释:

  • TokenLookup:在哪些部分去尝试读取 Token 一般情况下只用 header 一种就行了,在几天前的版本中还不支持在 form 中读取,现在支持了
  • TokenHeadName:Header 中 JWT 的前缀,这里就是 "Bearer {{JWT}}"

如果某个接口,可以登录也可以不登录,可以自定义一个中间件然后判断是否存在 Token,或者使用我下面这种不太好的方案

func OptionalJwtMiddlewareFunc() app.HandlerFunc {
	return func(ctx context.Context, req *app.RequestContext) {
		_, err := JwtMiddleware.ParseToken(ctx, req)
		if errors.Is(err, jwtmw.ErrEmptyFormToken) || errors.Is(err, jwtmw.ErrEmptyQueryToken) || errors.Is(err, jwtmw.ErrEmptyAuthHeader) {
			return
		}
		if err != nil {
			msg := JwtMiddleware.HTTPStatusMessageFunc(err, ctx, req)
			JwtMiddleware.Unauthorized(ctx, req, 200, msg)
			req.Abort()
			return
		}
		JwtMiddleware.MiddlewareFunc()(ctx, req)
	}
}