使用hertz的中间件JWT认证 | 青训营

288 阅读4分钟

JWT认证

官方文档

JWT原理
  • 传统Session认证:用户浏览器先发送账户密码,而后服务器在Session生成session_id发回给用户,用户之后的发送借助cookie将session_id发给服务器,由此服务器得到用户的身份。

    session认证的缺点:扩展性不好,每次认证都要在Session查找信息,一旦服务器发生故障,Session信息就会清零,用户就会退出登录。

  • 解决方案JWT:用户信息不保存在服务器,保存在客户端,每次请求发送用户信息即可。具体来说,将用户信息保存在JSON数据,服务器只靠JSON数据辨别用户身份,服务器变成无状态


  • JWT数据结构:header(头)+payload(数据)+signature(签名),中间用.拼接形成一个长字符串

    • 头信息是一个JSON对象,描述JWT的元信息,最重要的是alg属性表示签名的算法,最后将Base64转换为字符串

    • payload也是JSON对象,用来存放实际需要传递的信息,一般存放的是用户信息,这部分信息默认不加密,不要放置敏感信息,最后也用Base64加密。

    • 签名使用头信息指定的加密算法对(头+数据+服务器密钥)加密生成签名防止被入侵者篡改信息。

image-20230823233508709.png

  • JWT使用步骤:

    • 先要保证后端服务器(数据库)保存用户信息
    • 登录时,客户发送账户和密码,服务器生成JWT返回给客户端,客户端一般通过LocalStorage保存JWT信息。
    • 登录后,客户每次访问服务器都会在HTTP REQUEST HEADER中携带JWT,一般是以这样形式: 'Authorization: Bearer $token'
    • 服务器从JWT提取出用户信息验证。
Hertz-JWT
  1. 数据库需要提前存储好用户账户和密码。

  2. 登录

    在jwt.go,即jwt初始化的代码中需要手动设计的几个方法,先看相关所有代码:

    JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
        Key:           []byte("secret key"),
        Timeout:       time.Hour,
        MaxRefresh:    time.Hour,
        Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
            var loginStruct struct {
                Account  string `form:"account" json:"account" query:"account" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
                Password string `form:"password" json:"password" query:"password" vd:"(len($) > 0 && len($) < 30); msg:'Illegal format'"`
            }
            if err := c.BindAndValidate(&loginStruct); err != nil {
                return nil, err
            }
            users, err := mysql.CheckUser(loginStruct.Account, utils2.MD5(loginStruct.Password))
            if err != nil {
                return nil, err
            }
            if len(users) == 0 {
                return nil, errors.New("user already exists or wrong password")
            }
    ​
            return users[0], nil
        },
        PayloadFunc: func(data interface{}) jwt.MapClaims {
            if v, ok := data.(*model.User); ok {
                return jwt.MapClaims{
                    jwt.IdentityKey: v,
                }
            }
            return jwt.MapClaims{}
        },
    })
    
    • Authenticator:用于设置登录时认证用户信息的函数,demo 当中定义了一个 loginStruct 结构接收用户登陆信息(前端发送的数据JSON需要和该结构体对应上),并进行认证有效性(调用后端dao层API来验证)。这个函数的返回值 users[0] 将为后续生成 jwt token 提供 payload 数据源。
    • PayloadFunc:它的入参就是 Authenticator 的返回值,此时负责解析 users[0],并将用户名注入 token 的 payload 部分。
    • Key:指定了用于加密 jwt token 的密钥为 "secret key"
    • Timeout:指定了 token 有效期为一个小时。
    • MaxRefresh:用于设置最大 token 刷新时间,允许客户端在 TokenTime + MaxRefresh 内刷新 token 的有效时间,追加一个 Timeout 的时长。
  3. 返回token

    JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
        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,
                "expire":  expire.Format(time.RFC3339),
                "message": "success",
            })
        },
    })
    
    • LoginResponse:在登陆成功之后,jwt token 信息会随响应返回,你可以自定义这部分的具体内容,但注意不要改动函数签名,因为它与 LoginHandler 是强绑定的。
  4. Token的校验

JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    TokenLookup:   "header: Authorization, query: token, cookie: jwt",
    TokenHeadName: "Bearer",
    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":    code,
            "message": message,
        })
    },
})

访问配置了 jwt 中间件的路由时,会经过 jwt token 的校验流程。(登录、注册不会触发校验!!!!)

  • TokenLookup:用于设置 token 的获取源,可以选择 headerquerycookieparam,默认为 header:Authorization,同时存在是以左侧一个读取到的优先。当前 demo 将以 header 为数据源,因此在访问 /ping 接口时,需要你将 token 信息存放在 HTTP Header 当中。

  • TokenHeadName:用于设置从 header 中获取 token 时的前缀,默认为 "Bearer"

  • HTTPStatusMessageFunc:用于设置 jwt 校验流程发生错误时响应所包含的错误信息,你可以自行包装这些内容。

  • Unauthorized:用于设置 jwt 验证流程失败的响应函数,当前 demo 返回了错误码和错误信息。

  • 成功通过校验不会有返回函数!!!

5.用户信息的提取

JwtMiddleware, err = jwt.New(&jwt.HertzJWTMiddleware{
    IdentityKey: IdentityKey,
    IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
        claims := jwt.ExtractClaims(ctx, c)
        return &model.User{
            UserName: claims[IdentityKey].(string),
        }
    },
})

// Ping .
func Ping(ctx context.Context, c *app.RequestContext) {
    user, _ := c.Get(mw.IdentityKey)
    c.JSON(200, utils.H{
        "message": fmt.Sprintf("username:%v", user.(*model.User).UserName),
    })
}
  • IdentityHandler:用于设置获取身份信息的函数,在 demo 中,此处提取 token 的负载,并配合 IdentityKey 将用户名存入上下文信息。(如何调用获取信息是hertz代码需要考虑的
  • IdentityKey:用于设置检索身份的键,默认为 "identity"
  • Ping:构造响应结果,从上下文信息中取出用户名信息并返回。