golang iris的jwt实践

由于jwt原理已经有很多文章提及过了,这里不再赘述,本文主要介绍jwt在iris中的实践,文章的最后会给出完整代码,可以运行起来边测试边看。

如果文章对你有帮助,点个赞或者留下评论将会是对我的极大鼓励!

jwt使用方向

本文将jwt用于登录功能,如果是其他功能需求其实也类似,可以触类旁通。

iris中jwt的使用思想

iris基于中间件思想设计,对于一些重复性的操作,可以通过注册中间件来完成。对于登录功能,我们自然不希望每个api中手动判断是否过期,是否合法等,因为我们需要一个jwt中间件来完成这些操作。

获取jwt中间件

这是一个iris官方提供的jwt中间件:

import "github.com/iris-contrib/middleware/jwt"

用法如下:

j := jwt.New(jwt.Config{
    // Extractor属性可以选择从什么地方获取jwt进行验证,默认从http请求的header中的Authorization字段提取,也可指定为请求参数中的某个字段

    // 从请求参数token中提取
    // Extractor: jwt.FromParameter("token"),

    // 从请求头的Authorization字段中提取,这个是默认值
    Extractor: jwt.FromAuthHeader,

    // 设置一个函数返回秘钥,关键在于return []byte("这里设置秘钥")
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      return []byte("My Secret"), nil
    },

    // 设置一个加密方法
    SigningMethod: jwt.SigningMethodHS256,
  })
jwt.Config

使用jwt中间件

由于并非所有接口都需要设置登录拦截,所以将中间件用于路由,或者路由组是比较符合业务需求的做法

先给出两个接口:

  1. “/getJWT”:生成jwt并返回
  2. “/showHello”:没有任何限制,访问就输出”Hello Iris JWT”
package main

import (
  "github.com/kataras/iris/v12"

  "github.com/iris-contrib/middleware/jwt"
)

func main() {
  app := iris.New()

  app.Get("/getJWT", func(ctx iris.Context) {
    // 往jwt中写入了一对值
    token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "foo": "bar",
    })

    // 签名生成jwt字符串
    tokenString, _ := token.SignedString([]byte("My Secret"))

    // 返回
    ctx.JSON(tokenString)
  })

  app.Get("/showHello", func(ctx iris.Context) {
    ctx.JSON("Hello Iris JWT")
  })

  app.Run(iris.Addr(":8080"))
}

访问http://localhost:8080/getJWT返回一串类似这样的字符串

"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw"
"Hello Iris JWT"
showHello
package main

import (
  "github.com/kataras/iris/v12"

  "github.com/iris-contrib/middleware/jwt"
)

func main() {
  app := iris.New()

  j := jwt.New(jwt.Config{
    // Extractor属性可以选择从什么地方获取jwt进行验证,默认从http请求的header中的Authorization字段提取,也可指定为请求参数中的某个字段

    // 从请求参数token中提取
    // Extractor: jwt.FromParameter("token"),

    // 从请求头的Authorization字段中提取,这个是默认值
    Extractor: jwt.FromAuthHeader,

    // 设置一个函数返回秘钥,关键在于return []byte("这里设置秘钥")
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      return []byte("My Secret"), nil
    },

    // 设置一个加密方法
    SigningMethod: jwt.SigningMethodHS256,
  })

  app.Get("/getJWT", func(ctx iris.Context) {
    // 往jwt中写入了一对值
    token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "foo": "bar",
    })

    // 使用设置的秘钥,签名生成jwt字符串
    tokenString, _ := token.SignedString([]byte("My Secret"))

    // 返回
    ctx.JSON(tokenString)
  })

  // 注意这里加了j.Serve作为路由的中间件
  app.Get("/showHello", j.Serve, func(ctx iris.Context) {
    ctx.JSON("Hello Iris JWT")
  })

  app.Run(iris.Addr(":8080"))
}
required authorization token not found
HeaderAuthorizationgetJWT

测试JWT验证

PostmanHeaders
kEY VALUE
Authorization eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw
Authorization header format must be Bearer {token}
Authorization header format must be Bearer {token}HeadersAuthorization
PostmanHeadersAuthorization
kEY VALUE
Authorization JWT eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw
Bearer {token}
Authorization header format must be Bearer {token}

说明格式还是不对。

Headers
kEY VALUE
Authorization Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw
"Hello Iris JWT"
"Hello Iris JWT"

JWT中间件做了什么

/showHello"Hello Iris JWT"/showHello自动JWT验证
HeadersAuthorization
kEY VALUE
Authorization(真) Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw
Authorization(假) Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uu
signature is invalid

可见不合法的jwt是无法通过验证的。

获取JWT中的值

signature is invalid/showJWT
app.Get("/showJWT", j.Serve, func(ctx iris.Context) {
  jwtInfo := ctx.Values().Get("jwt").(*jwt.Token)
  ctx.JSON(jwtInfo)
})
Headers
kEY VALUE
Authorization Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw
{
    "Raw": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw",
    "Method": {
        "Name": "HS256",
        "Hash": 5
    },
    "Header": {
        "alg": "HS256",
        "typ": "JWT"
    },
    "Claims": {
        "foo": "bar"
    },
    "Signature": "4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw",
    "Valid": true
}
Claims
app.Get("/showJWTFoo", j.Serve, func(ctx iris.Context) {
  jwtInfo := ctx.Values().Get("jwt").(*jwt.Token)
  foo := jwtInfo.Claims.(jwt.MapClaims)["foo"].(string)
  ctx.JSON(foo)
})
Headers
kEY VALUE
Authorization Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJmb28iOiJiYXIifQ.4aQan-XvJBjDUDbCVnuh2P_xy54b2aRKKsgKcHUa8uw
"bar"

设置过期时间

由于jwt签发出去之后,后端并不存储,所以相当于后端并不能管理签发出去的jwt,这种情况下后端自然不能让签发出去的jwt永久有效,需要根据需求设置一个过期时间。

Claims
app.Get("/getJWTWithExp", func(ctx iris.Context) {
  token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
    // 根据需求,可以存一些必要的数据
    "userName": "JabinGP",
    "userId":   "1",
    "admin":    true,

    // 签发人
    "iss": "iris",
    // 签发时间
    "iat": time.Now().Unix(),
    // 设定过期时间,便于测试,设置1分钟过期
    "exp": time.Now().Add(1 * time.Minute * time.Duration(1)).Unix(),
  })

  // 使用设置的秘钥,签名生成jwt字符串
  tokenString, _ := token.SignedString([]byte("My Secret"))

  // 返回
  ctx.JSON(tokenString)
})
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTc1Mzc5OTk0LCJpYXQiOjE1NzUzNzk5MzQsImlzcyI6ImlyaXMiLCJ1c2VySWQiOiIxIiwidXNlck5hbWUiOiJKYWJpbkdQIn0.AMFZNbtERLHzKAR9z7oF_0QeNoARKr4dkjocNbVsWgg"
Headers
kEY VALUE
Authorization Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTc1Mzc5OTk0LCJpYXQiOjE1NzUzNzk5MzQsImlzcyI6ImlyaXMiLCJ1c2VySWQiOiIxIiwidXNlck5hbWUiOiJKYWJpbkdQIn0.AMFZNbtERLHzKAR9z7oF_0QeNoARKr4dkjocNbVsWgg
{
    "Raw": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTc1Mzc5OTk0LCJpYXQiOjE1NzUzNzk5MzQsImlzcyI6ImlyaXMiLCJ1c2VySWQiOiIxIiwidXNlck5hbWUiOiJKYWJpbkdQIn0.AMFZNbtERLHzKAR9z7oF_0QeNoARKr4dkjocNbVsWgg",
    "Method": {
        "Name": "HS256",
        "Hash": 5
    },
    "Header": {
        "alg": "HS256",
        "typ": "JWT"
    },
    "Claims": {
        "admin": true,
        "exp": 1575379994,
        "iat": 1575379934,
        "iss": "iris",
        "userId": "1",
        "userName": "JabinGP"
    },
    "Signature": "AMFZNbtERLHzKAR9z7oF_0QeNoARKr4dkjocNbVsWgg",
    "Valid": true
}

隔一分钟后,再次请求localhost:8080/showJWT返回

Token is expired

说明过期时间设置生效。

设置错误返回格式

由于JWT中间件在检验jwt发现不合法时,会自动响应返回错误信息并结束请求,如上所示,错误都是一句话,这样的错误对于前端来说非常不友好,前端更希望能通过一个状态码来判断请求是否顺利。

首先定义一个响应数据的模板:

// ResModel 返回数据模板
type ResModel struct {
  Code string      `json:"code"`
  Msg  string      `json:"msg"`
  Data interface{} `json:"data"`
}

获取一个新的中间件,与之前不同的是自定义了错误处理函数(扒了源码改的):

注意引入的context是iris包下的,否则无法符合错误处理函数的定义。

import "github.com/kataras/iris/v12/context"
// j2 对比 j 添加了错误处理函数
j2 := jwt.New(jwt.Config{
  // 注意,新增了一个错误处理函数
  ErrorHandler: func(ctx context.Context, err error) {
    if err == nil {
      return
    }

    ctx.StopExecution()
    ctx.StatusCode(iris.StatusUnauthorized)
    ctx.JSON(ResModel{
      Code: "501",
      Msg:  err.Error(),
    })
  },
  // 设置一个函数返回秘钥,关键在于return []byte("这里设置秘钥")
  ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
    return []byte("My Secret"), nil
  },

  // 设置一个加密方法
  SigningMethod: jwt.SigningMethodHS256,
})
j2
app.Get("/showJWTErrWithFormat", j2.Serve, func(ctx iris.Context) {
  jwtInfo := ctx.Values().Get("jwt").(*jwt.Token)
  ctx.JSON(jwtInfo)
})
Headers
kEY VALUE
Authorization Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTc1Mzc5OTk0LCJpYXQiOjE1NzUzNzk5MzQsImlzcyI6ImlyaXMiLCJ1c2VySWQiOiIxIiwidXNlck5hbWUiOiJKYWJpbkdQIn0.AMFZNbtERLHzKAR9z7oF_0QeNoARKr4dkjocNbVsWgg
{
    "code": "501",
    "msg": "Token is expired",
    "data": null
}
HeadersAuthorizationgo
kEY VALUE
Authorization Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6dHJ1ZSwiZXhwIjoxNTc1Mzc5OTk0LCJpYXQiOjE1NzUzNzk5MzQsImlzcyI6ImlyaXMiLCJ1c2VySWQiOiIxIiwidXNlck5hbWUiOiJKYWJpbkdQIn0.AMFZNbtERLHzKAR9z7oF_0QeNoARKr4dkjocNbVsWgo
{
    "code": "501",
    "msg": "signature is invalid",
    "data": null
}

至此,jwt基本符合使用需求了,最后附上完整的代码。

完整代码

如果文章对你有帮助,点个赞或者留下评论将会是对我的极大鼓励!

package main

import (
  "time"

  "github.com/kataras/iris/v12"

  "github.com/iris-contrib/middleware/jwt"
  "github.com/kataras/iris/v12/context"
)

// ResModel 返回数据模板
type ResModel struct {
  Code string      `json:"code"`
  Msg  string      `json:"msg"`
  Data interface{} `json:"data"`
}

func main() {
  app := iris.New()

  j := jwt.New(jwt.Config{
    // Extractor属性可以选择从什么地方获取jwt进行验证,默认从http请求的header中的Authorization字段提取,也可指定为请求参数中的某个字段

    // 从请求参数token中提取
    // Extractor: jwt.FromParameter("token"),

    // 从请求头的Authorization字段中提取,这个是默认值
    Extractor: jwt.FromAuthHeader,

    // 设置一个函数返回秘钥,关键在于return []byte("这里设置秘钥")
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      return []byte("My Secret"), nil
    },

    // 设置一个加密方法
    SigningMethod: jwt.SigningMethodHS256,
  })

  // j2 对比 j 添加了错误处理函数
  j2 := jwt.New(jwt.Config{
    // 注意,新增了一个错误处理函数
    ErrorHandler: func(ctx context.Context, err error) {
      if err == nil {
        return
      }

      ctx.StopExecution()
      ctx.StatusCode(iris.StatusUnauthorized)
      ctx.JSON(ResModel{
        Code: "501",
        Msg:  err.Error(),
      })
    },
    // 设置一个函数返回秘钥,关键在于return []byte("这里设置秘钥")
    ValidationKeyGetter: func(token *jwt.Token) (interface{}, error) {
      return []byte("My Secret"), nil
    },

    // 设置一个加密方法
    SigningMethod: jwt.SigningMethodHS256,
  })

  app.Get("/getJWT", func(ctx iris.Context) {
    // 往jwt中写入了一对值
    token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      "foo": "bar",
    })

    // 使用设置的秘钥,签名生成jwt字符串
    tokenString, _ := token.SignedString([]byte("My Secret"))

    // 返回
    ctx.JSON(tokenString)
  })
  app.Get("/getJWTWithExp", func(ctx iris.Context) {
    token := jwt.NewTokenWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
      // 根据需求,可以存一些必要的数据
      "userName": "JabinGP",
      "userId":   "1",
      "admin":    true,

      // 签发人
      "iss": "iris",
      // 签发时间
      "iat": time.Now().Unix(),
      // 设定过期时间,便于测试,设置1分钟过期
      "exp": time.Now().Add(1 * time.Minute * time.Duration(1)).Unix(),
    })

    // 使用设置的秘钥,签名生成jwt字符串
    tokenString, _ := token.SignedString([]byte("My Secret"))

    // 返回
    ctx.JSON(tokenString)
  })

  app.Get("/showHello", j.Serve, func(ctx iris.Context) {
    ctx.JSON("Hello Iris JWT")
  })

  app.Get("/showJWT", j.Serve, func(ctx iris.Context) {
    jwtInfo := ctx.Values().Get("jwt").(*jwt.Token)
    ctx.JSON(jwtInfo)
  })

  app.Get("/showJWTFoo", j.Serve, func(ctx iris.Context) {
    jwtInfo := ctx.Values().Get("jwt").(*jwt.Token)
    foo := jwtInfo.Claims.(jwt.MapClaims)["foo"].(string)
    ctx.JSON(foo)
  })

  app.Get("/showJWTErrWithFormat", j2.Serve, func(ctx iris.Context) {
    jwtInfo := ctx.Values().Get("jwt").(*jwt.Token)
    ctx.JSON(jwtInfo)
  })

  app.Run(iris.Addr(":8080"))
}