jwt实践之gin-jwt | 青训营笔记

373 阅读6分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第5篇笔记。

1 JWT

1.1 JWT 简介

HTTP/HTTPS 是无状态的,所以无法完成认证服务。

常见的认证方法有 cookie 和 session,因为 cookie 的保密性问题,所以登录的认证鉴权一般不会使用 cookie。使用 session 的缺点在于,服务器要保存 session 数据,单机情况下是可以的,如果服务器是集群的,或者是跨域的服务导向架构,就要求 session 数据共享,这就会带来一系列的问题。

解决方法是服务器中不保存 session 的数据,所有数据都保存在客户端,每次请求都带着认证相关的数据。JWT就使用了这种方案。

JWT 是 JSON Web Token的缩写。好处是服务端就是无状态的了,这样服务端更容易进行扩展。

1.2 JWT 原理

JWT 是一个字符串,中间用 . 分成三个部分

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NTUwMjMyOTQsImlkIjoxLCJvcmlnX2lhdCI6MTY1NTAxOTY5NH0.pBpZDyrwE2vw8h7bEV7FICobJmgbX9aVtMZFPoLVoNg

这三个部分是:

  • Header(头部)
  • Payload(负载)
  • Signature(签名)

所以 JWT 的结构就是 Header.Payload.Signature

前两个部分是 JSON 使用 Base64URL 算法转码后的结果, Signature是对Header.Payload的签名,为了防止数据的篡改。

注下面提到的 Header 和 Payload 都是指使用 Base64URL 编码后的字符串

1.2.1 JWT 结构

Header

Header 解码出来的 JSON 对象通常是下面的样子

{
  "alg": "HS256",
  "typ": "JWT"
}

alg 属性表示签名的算法,typ 表示这个令牌(token)的类型,JWT令牌的值是JWT

Payload

Payload 解码出来的 JSON 对象用来存放实际需要传递的数据,JWT 规定了7个官方字段,供选用

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):主题
  • aud (audience):受众
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

除了官方字段之外,我们还可以使用自定义的字段,比如存放用户的 id, name等:

{
  "id": 123456,
  "name": root
}

Payload 字段一般存放什么(一般会存放用户id),用来认证用户

在实际项目中,我们一般会在Payload中存放后端需要的认证信息。

Signature

如果没有 Signature,JWT 就会变得和 cookie 一样不安全

Signature 是对前两部分的签名,防止数据篡改

Signature 是通过使用 Header 里面指定的签名算法(默认是 HMAC SHA256)和指定的密钥对Header.Payload 进行加密后的结果,密钥只有服务端知道。

服务端在收到 JWT 之后使用 Header 里指定的签名算法和指定的密钥对Header.Payload进行加密,如果和 Signature 一致,就说明 JWT 没有被篡改

使用方式

客户端每次与服务端进行通信都要携带 JWT,可以:将 JWT 放在 Cookie 里面自动发送,但是不能跨域。

下面的方式能解决跨域问题,但是每次请求的时候都要手动带上 JWT:

  1. 将 JWT 放在 HTTP 请求头的 Authorization 字段里面
  2. 作为 HTTP 的请求参数,比如 http://xxx.com/request?token=<token>
  3. 如果是 POST 请求的话,还可以放到请求体中

之后我们会看到 gin-jwt 是怎么实现这几种方式的

如何将 token 携带在请求中发送给服务器

注意:

JWT 不是加密的,Signature的作用就是保证JWT没有被篡改,而Header和Payload也很容易解码出来其中的JSON对象,所以

base64是用来转码的,不是用来加密的,也就是说通过base64转码后的字符串,可以轻易获取原数据

2 JWT 的一种实现 gin-jwt

gin-jwt 是 JWT 的一种实现,能够很好的支持 gin 框架的认证鉴权。

这里只介绍 gin-jwt 的基本使用,部分没有介绍的可以查看官方文档:github.com/appleboy/gi…

  authMiddleware, err := jwt.New(&jwt.GinJWTMiddleware{
    Key:         []byte("secret key"),
    Timeout:     time.Hour,
    MaxRefresh:  time.Hour,
    IdentityKey: identityKey,
    PayloadFunc: func(data interface{}) jwt.MapClaims {
      if v, ok := data.(*User); ok {
        return jwt.MapClaims{
          identityKey: v.UserName,
        }
      }
      return jwt.MapClaims{}
    },
    Authenticator: func(c *gin.Context) (interface{}, error) {
      var loginVals login
      if err := c.ShouldBind(&loginVals); err != nil {
        return "", jwt.ErrMissingLoginValues
      }
      userID := loginVals.Username
      password := loginVals.Password

      if (userID == "admin" && password == "admin") || (userID == "test" && password == "test") {
        return &User{
          UserName:  userID,
          LastName:  "Bo-Yi",
          FirstName: "Wu",
        }, nil
      }

      return nil, jwt.ErrFailedAuthentication
    },
    Unauthorized: func(c *gin.Context, code int, message string) {
      c.JSON(code, gin.H{
        "code":    code,
        "message": message,
      })
    },
    // TokenLookup is a string in the form of "<source>:<name>" that is used
    TokenLookup: "header: Authorization, query: token, cookie: jwt",
    
    TokenHeadName: "Bearer",
    
    TimeFunc: time.Now,
  })
  • Key: 有服务器保管的密钥,用来生成签名
  • Authenticator: 用来做用户的认证服务,比如通过用户名和密码来认证用户,认证后会返回用户的相关信息,这些信息会被传递给PayloadFunc
  • PayloadFunc: 用来设置 JWT 的 Payload 中的数据,会返回一个Map结构的数据
  • TokenLookup: 表明传递的 token 所在的位置,前面提到可以通过cookie,请求参数,Header等方式传递 token(注意,gin-jwt 没有实现从消息体中获取token),默认值是 "header:Authorization",表示从 Header 里的 Authorization 字段中寻找 token
  • TokenHeadName: 当通过HTTP的Header传递 token 时的名称,默认值是 Bearer,比如在 Header 中传递 token:Authorization: Bearer <token>
  • TimeFunc: 提供当前时间,当token的时区和服务器的时区不一致时可以自定义TimeFunc

接下来我们看一个认证的例子:用户登录

首先前端会传入用户名和密码,后端在判断密码正确后要生成JWT返回给前端,我们可以看看gin-jwt是如何做这件事情的。

gin-jwt 提供了一个 LoginHandler 可以使我们很方便地完成这件事情,LoginHandler 的大概流程(不是完全的)是:

image.png

Authenticator 和 PayloadFunc 上面解释过。

注意其中,unautorized 函数是不能够自定义的,其他的都可以自定义,但是 unauthorized 函数会调用 Unauthorized 函数,而 Unauthoried 函数是可以自定义的,通过修改 Unauthorized 函数,我么可以指定认证失败的返回值。

可以通过自定义 LoginResponse 函数来修改认证成功的返回值,默认的返回是:

{
  "code": http.StatusOK,
  "token": token,
  "expire": expire.Format(time.RFC3339)
}

也就是说,在登录成功后,我们就可以获得 token 了,之后再发送请求的时候就可以携带 token 来认证了。

那服务器是怎样处理 token 呢,gin-jwt 提供了一个中间件函数 MiddlewareFunc() 来处理认证,它会提取 token 中的数据,检验 token 是否合法等,一般在需要认证的接口中使用这个中间件,下面是 gin 中的一个例子

user := r.Group("/user")
user.User(authMiddleware.MiddlewareFunc())
{
    api.GET("/info", controller.GetUserInfo)
    api.POST("/logout", controller.Logout)
}

MiddlewareFunc() 的具体流程是:

image.png

  • 寻找 token: 是根据前面提到的 TokenLookup 参数指定的位置进行寻找
  • 解析 token: 通过Header部分指定的签名算法和密钥 Key,判断token是否有效,有效则解析 token 获取 token 中的 claims 部分(对应前面的 Payload),返回的 claims 是一个 map 结构
  • 然后从 claims 中获取 exp 字段来判断 token 是否过期
  • 如果没过期,就将 claims 写入 gin.Context 中的 "JWT_PAYLOAD": c.Set("JWT_PAYLOAD", claims),之后的 handler 就可以通过 gin-jwt 提供的 ExtractClaims 函数从 gin.Context 中获取 claims
  • 最后,调用下一个 HandlerFunc:c.Next()