在前后端分离的项目中,越来越多的项目采用 JWT 代替传统的 cookie ,这里我们来使用 JWT 结合 Gin 来作为一个登录授权和权限校验。

🔑什么是 JWT

JWT 的全称叫做 JSON WEB TOKEN,在目前前后端系统中使用较多。

JWT 构成

JWT 是由三段构成的。分别是 HEADER,PAYLOAD,VERIFY SIGNATURE,它们生成的信息通过 . 分割。

header 是由 一个 typalg 组成,typ 会指明为 JWT,而 alg 是所使用的加密算法。

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

PAYLOAD

payload 是 JWT 的载体,也就是我们要承载的信息。这段信息是我们可以自定义的,可以定义我们要存放什么信息,那些字段。该部分信息不宜过多,它会影响 JWT 生成的大小,还有就是请勿将敏感数据存入该部分,该端数据前端是可以解析获取 token 内信息的。

官方给了七个默认字段,我们可以不全部使用,也可以加入我们需要的字段。

名称含义
Audience表示JWT的受众
ExpiresAt失效时间
Id签发编号
IssuedAt签发时间
Issuer签发人
NotBefore生效时间
Subject主题

VERIFY SIGNATURE

这也是 JWT 的最后一段,该部分是由算法计算完成的。

对刚刚的 header 进行 base64Url 编码,对 payload 进行 base64Url 编码,两端完成编码后通过 . 进行连接起来。

base64UrlEncode(header).base64UrlEncode(payload)

完成上述步骤后,就要通过我们 header 里指定的加密算法对上部分进行加密,同时我们还要插入我们的一个密钥,来确保我的 JWT 签发是安全的。

这便是我们的第三部分。

当三部分都完成后,通过使用 . 将三部分分割,生成了上图所示的 JWT 。

JWT 登录原理

简单的说就是当用户登录的时候,服务器校验登录名称和密码是否正确,正确的话,会生成 JWT 返回给客户端。客户端获取到 JWT 后要进行保存,之后的每次请求都会讲 JWT 携带在头部,每次服务器都会获取头部的 JWT 是否正确,如果正确则正确执行该请求,否者验证失败,重新登录。

🔒Gin 生成 JWT

go 语言的 JWT 库有很多。jwt.io 上也给出了很多 。这里使用 jwt-go

"github.com/dgrijalva/jwt-go"

我们对登录方法进行改造。

// 省略代码
expiresTime := time.Now().Unix() + int64(config.OneDayOfHours)
claims := jwt.StandardClaims{
    Audience:  user.Username,     // 受众
    ExpiresAt: expiresTime,       // 失效时间
    Id:        string(user.ID),   // 编号
    IssuedAt:  time.Now().Unix(), // 签发时间
    Issuer:    "gin hello",       // 签发人
    NotBefore: time.Now().Unix(), // 生效时间
    Subject:   "login",           // 主题
}
var jwtSecret = []byte(config.Secret)
tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 省略代码

这里的 config.OneDayOfHours 设定了过期时间,这里设定了一天。通过 StandardClaims 生成标准的载体,也就是上文提到的七个字段,其中 编号设定为 用户 id。其中的 jwtSecret 是我们设定的密钥,

我们这里通过 HS256 算法生成 tokenClaims ,这就是我们的 HEADER 部分和 PAYLOAD。

token, err := tokenClaims.SignedString(jwtSecret)

这样便生成了我们的 token 。我们要将我们的 token 和 Bearer 拼接在一起,同时中间用空格隔开。

token =  "Bearer "+ token

生成 Bearer Token 。

当我们用户进行登录的时候,就可以通过该片段生成 JWT。

下面是完整代码:

func CreateJwt(ctx *gin.Context) {
	// 获取用户
	user := &model.User{}
	result := &model.Result{
		Code:    200,
		Message: "登录成功",
		Data:    nil,
	}
	if e := ctx.BindJSON(&user); e != nil {
		result.Message = "数据绑定失败"
		result.Code = http.StatusUnauthorized
		ctx.JSON(http.StatusUnauthorized, gin.H{
			"result": result,
		})
	}
	u := user.QueryByUsername()
	if u.Password == user.Password {
		expiresTime := time.Now().Unix() + int64(config.OneDayOfHours)
		claims := jwt.StandardClaims{
			Audience:  user.Username,     // 受众
			ExpiresAt: expiresTime,       // 失效时间
			Id:        string(user.ID),   // 编号
			IssuedAt:  time.Now().Unix(), // 签发时间
			Issuer:    "gin hello",       // 签发人
			NotBefore: time.Now().Unix(), // 生效时间
			Subject:   "login",           // 主题
		}
		var jwtSecret = []byte(config.Secret)
		tokenClaims := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
		if token, err := tokenClaims.SignedString(jwtSecret); err == nil {
			result.Message = "登录成功"
			result.Data = "Bearer " + token
			result.Code = http.StatusOK
			ctx.JSON(result.Code, gin.H{
				"result": result,
			})
		} else {
			result.Message = "登录失败"
			result.Code = http.StatusOK
			ctx.JSON(result.Code, gin.H{
				"result": result,
			})
		}
	} else {
		result.Message = "登录失败"
		result.Code = http.StatusOK
		ctx.JSON(result.Code, gin.H{
			"result": result,
		})
	}
}

通过 .http 请求测试,结果如下

{
  "result": {
    "code": 200,
    "message": "登录成功",
    "data": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjMiLCJleHAiOjE1NjQ3OTY0MzksImp0aSI6Ilx1MDAwMCIsImlhdCI6MTU2NDc5NjQxOSwiaXNzIjoiZ2luIGhlbGxvIiwibmJmIjoxNTY0Nzk2NDE5LCJzdWIiOiJsb2dpbiJ9.CpacmfBSMgmK2TgrT-KwNB60bsvwgyryGQ0pWZr8laU"
  }
}

这个便完成了token的生成。

🔐Gin 校验 Token

那么,接下来就需要完成 token 的验证。

还记得之前我们验证用户是否授权采用的办法吗?是的,在中间件里查看用户 cookie。同样的方法,我们这里校验用户 JWT 是否有效。

编写我们的中间件。

新建立 middleware/Auth.go

首先先编写我们的解析 token 方法,parseToken()

func parseToken(token string) (*jwt.StandardClaims, error) {
	jwtToken, err := jwt.ParseWithClaims(token, &jwt.StandardClaims{}, func(token *jwt.Token) (i interface{}, e error) {
		return []byte(config.Secret), nil
	})
	if err == nil && jwtToken != nil {
		if claim, ok := jwtToken.Claims.(*jwt.StandardClaims); ok && jwtToken.Valid {
			return claim, nil
		}
	}
	return nil, err
}

通过传入我们的 token , 来对 token 进行解析。

完整的中间件代码

func Auth() gin.HandlerFunc {
	return func(context *gin.Context) {
		result := model.Result{
			Code:    http.StatusUnauthorized,
			Message: "无法认证,重新登录",
			Data:    nil,
		}
		auth := context.Request.Header.Get("Authorization")
		if len(auth) == 0 {
			context.Abort()
			context.JSON(http.StatusUnauthorized, gin.H{
				"result": result,
			})
		}
		auth = strings.Fields(auth)[1]
		// 校验token
		_, err := parseToken(auth)
		if err != nil {
			context.Abort()
			result.Message = "token 过期" + err.Error()
			context.JSON(http.StatusUnauthorized, gin.H{
				"result": result,
			})
		} else {
			println("token 正确")
		}
		context.Next()
	}
}

首先在请求头获取 token ,然后对先把 token 进行解析,将 Bearer 和 JWT 拆分出来,将 JWT 进行校验。

我们只需要对我们需要校验的路由进行添加中间件校验即可。

	router.GET("/", middleware.Auth(), func(context *gin.Context) {
		context.JSON(http.StatusOK, time.Now().Unix())
	})

当我们访问 / 的时候就需要携带 token 了

GET http://localhost:8080
Content-Type: application/json
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiIxMjMiLCJleHAiOjE1NjQ3OTQzNjIsImp0aSI6Ilx1MDAwMCIsImlhdCI6MTU2NDc5NDM0MiwiaXNzIjoiZ2luIGhlbGxvIiwibmJmIjoxNTY0Nzk0MzQyLCJzdWIiOiJsb2dpbiJ9.uQxGMsftyVFtYIGwQVm1QB2djw-uMfDbw81E5LMjliU

✍总结

本章节对什么是 JWT,Gin 中如何使用 JWT 做了介绍。但是不要过于迷信 JWT,JWT 还有很多问题,比如说 JWT 失效只能是时间过期,如果修改密码或者账户注销等操作需要我们另外添加逻辑判断。适合的地方选用适合的技术才能发挥最大的优势。

👨‍💻本章节代码

Github