JWT中间件学习笔记 | 青训营笔记

159 阅读5分钟

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

1 json web 令牌结构

jwt全称 Json web token,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其他业务逻辑所必须的声明信息,该token也可直接被用于认证,也可以被加密。官网点击此处

JSON Web令牌由三部分组成,这些部分由.分隔,分别是

  • Header
  • Payload
  • Signature

一个JWT表示如示例:xxxxx.yyyyy.zzzzz

1.1 Header

Header通常由两部分组成:令牌的类型和所使用的签名算法,例如HMAC、SHA256或者RSA。

//此json是由Base64Url编码形成JWT的第一部分。
{
  "alg": "HS256",
  "typ": "JWT"
}

1.2 Payload

令牌的第二部分是有效载荷。用于声明,通常存储一些用户ID之类的索引数据,也可以放一些其他有用的信息,注意:不要存储机密数据。JWT标准定义了一些基本字段:

  • iss:该JWT的签发者
  • sub:该JWT所面向的用户
  • aud:接收该JWT的一方
  • exp(expires):过期时间
  • iat:签发时间

除了定义这几个标准字段外,我们可以定义一些我们在业务处理中需要用到的字段,可以有用户的id、名字等。来个例子看一看吧:

{
    "iss": "asong",
    "iat": 6666666666,
    "exp": 6666666666,
    "aud": "user",
    "sub": "all",
    "user_id": "6666666666666666666",
    "username": "asong"
}
//上面的user_id、username都是我们定义的字段。对此负载进行Base64Url编码,形成JSON Web令牌的第二部分。

1.3 Signature

签名其实是对JWT的Header和Payload整合的一个签名验证。我们需要将Header和Payload链接起来,然后使用一个key用HMAC SHA256进行加密,创建一个签名,像下面这样。

HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)

签名的作用用于验证消息在此过程中没有更改,并且对于使用私钥进行签名的令牌,它还可以验证JWT的发件人是谁。

2 什么时候使用JWT

Authorization(授权):用户请求的token中包含了该令牌允许的路由,服务和资源。单点登录就是其中广泛使用JWT的一个特性。

Information Exchange(信息交换):对于安全的在各方之间传输信息而言,JSON Web Tokens无疑是一种很好的方式。因为JWTs可以被签名,例如,用公钥/私钥对,可以确定发送人身份。并且签名是使用头和有效负载计算的,还可以验证内容有没有被篡改。

image.png

根据上图所示,我们可以看到整个过程分为两个阶段,第一个阶段,客户端向服务器获取token,第二阶段,客户端带着该token去请求相关的资源。服务端通常根据指定的规则进行token的生成。在认证的时候,当用户用他们的凭证成功登录以后,一个JSON WebToken将会被返回。这是这个token就是用户凭证了,我们必须小心防止出现安全问题。一般我们保存令牌的时候不应该超过你所需要他的时间。无论何时用户想要访问受保护的路由或者资源的时候,用户代理(通常是浏览器)都应该带上JWT,典型的,通常放在Authorization header中,用Bearer schema: Authorization: Bearer 。服务器上的受保护的路由将会检查Authorization header中的JWT是否有效,如果有效,则用户可以访问受保护的资源。如果JWT包含足够多的必需的数据,那么就可以减少对某些操作的数据库查询的需要,尽管可能并不总是如此。如果token是在授权头(Authorization header)中发送的,那么跨源资源共享(CORS)将不会成为问题,因为它不使用cookie.

3 解析hertz-JWT的初始化参数

func InitJWT() {
    JwtMiddleware, _ = jwt.New(&jwt.HertzJWTMiddleware{
        Key:           []byte(shared.SecretKey),
        TokenLookup:   "form: token, param: token, header: Authorization, query: token",//这里主要用到form和query
        TokenHeadName: "Bearer",
        TimeFunc:      time.Now,
        Timeout:       time.Hour, //指定了 token 有效期为一个小时
        MaxRefresh:    time.Hour, //用于设置最大 token 刷新时间
        IdentityKey:   shared.IdentityKey, // 用于设置检索身份的键
        /*下面这个函数专用于中间件*/
        //用于设置获取身份信息的函数,此处提取 token 的负载,并配合 IdentityKey 将用户id存入上下文信息。
        IdentityHandler: func(ctx context.Context, c *app.RequestContext) interface{} {
            claims := jwt.ExtractClaims(ctx, c)
            return &api.User{
                Id: int64(claims[shared.IdentityKey].(float64)),
            }
        },
        //这个是用来登录验证的--Given the user identity value (data parameter) and the hertz context,
        //这个函数的返回值 rpc.CheckUser的返回值 将为后续生成 jwt token 提供 payload 数据源。
        Authenticator: func(ctx context.Context, c *app.RequestContext) (interface{}, error) {
            var err error
            var req api.DouyinUserLoginRequest
            if err = c.BindAndValidate(&req); err != nil {
                return "", jwt.ErrMissingLoginValues
            }
            if len(req.Username) == 0 || len(req.Password) == 0 {
                return "", jwt.ErrMissingLoginValues
            }
            return rpc.CheckUser(context.Background(), &UserServer.DouyinUserLoginRequest{
                Username: req.Username,
                Password: req.Password,
            })
        },
        //它的入参就是 Authenticator 的返回值,此时负责解析 users,并将用户id注入 token 的 payload 部分
        PayloadFunc: func(data interface{}) jwt.MapClaims {
            if v, ok := data.(int64); ok {
                return jwt.MapClaims{
                    shared.IdentityKey: v,//这里的IdentityKey为id
                }
            }
            return jwt.MapClaims{}
        },
        //用于设置登录的响应函数--Token 的返回
        LoginResponse: func(ctx context.Context, c *app.RequestContext, code int, token string, expire time.Time) {
            c.JSON(http.StatusOK, utils.H{
                "code":   errno.Success.ErrCode,
                "token":  token,
                "expire": expire.Format(time.RFC3339),
                "message": "success",
            })
        },
        /*访问配置了 jwt 中间件的路由时,会经过 jwt token 的校验流程。*///用于设置 jwt 验证流程失败的响应函数,当前 demo 返回了错误码和错误信息。
        Unauthorized: func(ctx context.Context, c *app.RequestContext, code int, message string) {
            c.JSON(http.StatusOK, utils.H{
                "code":    errno.AuthorizationFailedErr.ErrCode,
                "message": message,
            })
        },
        //用于设置 jwt 校验流程发生错误时响应所包含的错误信息,你可以自行包装这些内容
        HTTPStatusMessageFunc: func(e error, ctx context.Context, c *app.RequestContext) string {
            switch t := e.(type) {
            case errno.ErrNo:
                return t.ErrMsg
            default:
                return t.Error()
            }
        },
    })

4 由用户名生成token

// TokenGenerator method that clients can use to get a jwt token.
func (mw *HertzJWTMiddleware) TokenGenerator(data interface{}) (string, time.Time, error) {
    token := jwt.New(jwt.GetSigningMethod(mw.SigningAlgorithm))
    claims := token.Claims.(jwt.MapClaims)
​
    if mw.PayloadFunc != nil {
        for key, value := range mw.PayloadFunc(data) {
            claims[key] = value
        }
    }
​
    expire := mw.TimeFunc().UTC().Add(mw.Timeout)
    claims["exp"] = expire.Unix()
    claims["orig_iat"] = mw.TimeFunc().Unix()
    tokenString, err := mw.signedString(token)
    if err != nil {
        return "", time.Time{}, err
    }
​
    return tokenString, expire, nil
}