gin框架JWT验证实践(原理介绍,代码实践)

704 阅读7分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

前言

  作者大二,在网上看各种名词,什么cookie,session,JWT,看了几篇文章后,概念是理解了,但是真正想写一个小demo实践的时候,==总是感觉无从下手==,所以写本文的目的,意在帮助在与我同阶段的同学,==从0到1的去梳理出流程,以提升自己的抽象和分析能力==。

前置要求:文demo使用了gin和gorm,如果不知道gin和gorm两个库,如果没有使用过的需要提前去了解一下基本用法即可 //TODO 挖坑 gin 和 gorm 相关内容 后续补充 GORM汇总

demo链接:gopherWxf-wake

JWT介绍

背景

  在如今前后端分离开发的⼤环境中,我们需要解决⼀些登陆,后期身份认证以及鉴权相关的事情,通常的⽅案就是采⽤请求头携带token的⽅式进⾏实现。本篇⽂章主要分享下在Golang语⾔下使⽤jwt-go来实现后端的token认证逻辑。   JSON Web Token(JWT) 是⼀个常⽤语HTTP的客户端和服务端间进⾏身份认证和鉴权的标准规范,使⽤JWT可以允许我们在⽤户和服务器之间传递安全可靠的信息。在开始学习JWT之前,我们可以先了解下早期的⼏种⽅案。

token

token

  token的意思是“令牌”,是⽤户身份的验证⽅式,最简单的token组成: uid(⽤户唯⼀标识) + time(当前时间戳) + sign(签名,由token的前⼏位+哈希算法压缩成⼀定⻓度的⼗六进制字符串) ,同时还可以将不变的参数也放进token,这⾥我主要想讲的就是 Json Web Token ,也就是本文的主题:JWT

Json-Web-Token(JWT)介绍

  ⼀般⽽⾔,⽤户注册登陆后会⽣成⼀个jwt的token返回给浏览器,浏览器向服务端请求数据时携带token ,服务器端使⽤ header 中定义的⽅式进⾏解码,进⽽对token进⾏解析和验证。

JWT-Token组成部分:

  • ==header==: ⽤来指定使⽤的算法(HMAC SHA256 RSA)和token类型(如JWT)
  • ==payload==: 包含声明(要求),声明通常是⽤户信息或其他数据的声明,⽐如⽤户id,名称,邮箱等
  • ==signature==: signature签名部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。

首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名

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

在这里插入图片描述

  签名是(signature) ⽤于验证消息在传递过程中有没有被更改 ,并且,对于使⽤私钥签名的token,它还可以验证JWT的发送⽅是否为它所称的发送⽅。

注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后:

  • header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据
  • signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secret对header、payload进行加密,==比对加密后的数据和客户端发送过来的是否一致==。注意secret只能保存在服务端。

在这里插入图片描述

  ⾃⼰计算出来的签名和接受到的签名不⼀样,那么就说明这个Token的内容被别⼈动过的,我们应该拒绝这个Token,返回⼀个HTTP 401 Unauthorized响应。   注意:在JWT中,不应该在载荷⾥⾯加⼊任何敏感的数据,⽐如⽤户的密码。

JSON Web TokenS - jwt.io在jwt.io⽹站中,提供了⼀些JWT token的编码,验证以及⽣成jwt的⼯具。

可以看到header 和 payload 部分可以直接解码出来 此时签名为kV8BkKaSKlugSV6moWGJs87lS0hyNnqHYzroqKSGs-8'

kV8BkKaSKlugSV6moWGJs87lS0hyNnqHYzroqKSGs-8=HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

如果篡改了header或者payload的内容,那么计算hs256的值一定不一样

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

在这里插入图片描述

介绍完jwt的原理,下面我们就该动手实践了!

Demo代码分析

流程分析

流程分析:

  1. 什么时候创建token并返回给前端呢?用户登陆的时候,因为接下来的网页用户不需要再次登陆才能访问了
  2. 前端用户携带token访问后端的时候,后端需要干什么呢?后端需要校验token是否合法,如果合法则继续执行业务逻辑,如果不合法直接返回错误,中断本次业务逻辑

好,初步的流程是

  1. 登陆的时候后端创建token返回给前端
  2. 除了登陆和注册的接口,其他接口都携带该token访问后端

在这里插入图片描述

框架分析

我们首先需要对demo的项目注册有个大概的了解

在这里插入图片描述

demo演示

注册: 在这里插入图片描述 登陆: 在这里插入图片描述 访问测试接口: 在这里插入图片描述 更新token时间:

在这里插入图片描述

后端日志打印:

在这里插入图片描述

走入源码

入口main:

首先我们的程序需要连接数据库(因为用户注册和登陆需要数据库),定义4个接口

POST   /apis/v1/register         --> jwtDemo/controller.RegisterUser  //注册
POST   /apis/v1/login            --> jwtDemo/controller.Login         //登陆

下面两个接口都有一个token校验的中间件sv1.Use(middleware.JWTAuth)

GET    /apis/v1/auth/sayHello    --> jwtDemo/controller.SayHello      //测试
GET    /apis/v1/auth/refresh     --> jwtDemo/controller.Refresh       //更新token
func main() {
	//从配置文件中读取数据库的配置信息并连接数据库
	if dbErr := opdb.InitMySqlConn(); dbErr != nil {
		log.Panicln(dbErr)
	}
	defer opdb.DB.Close()
	//初始化表结构
	opdb.InitModel()
	router := gin.Default()
	v1 := router.Group("apis/v1")
	{ 	//注册
		v1.POST("/register", controller.RegisterUser)
		//登陆,返回token
		v1.POST("/login", controller.Login)
	}
	sv1 := v1.Group("/auth")
	//检验token
	sv1.Use(middleware.JWTAuth)
	{ 	//测试
		sv1.GET("/sayHello", controller.SayHello)
		//更新token
		sv1.GET("/refresh", controller.Refresh)
	}
	router.Run(":8080")
}
  • controller.RegisterUser:前端返回的json,包含用户名,密码,手机号,地址等然后后端存入数据库

  • controller.Login:通过前端返回的json,查询数据库中是否有该用户,有则创建一个json并返回

  • middleware.JWTAuth:校验改token是否合法,如果合法则继续执行,不合法直接结束流程

  • controller.SayHello:通过了JWTAuth的校验,改前端返回一个hello wxf

  • controller.Refresh:通过了JWTAuth的校验,给token续费,返回一个新的token

好,现在各个回调函数的流程都知道了,我们详细来看看创建token和校验token的代码

创建token:

  1. 创建一个包含secret的结构体
  2. 构造用户claims信息(负荷jwt中的第二部分)
  3. 通过secret密钥返回token
	//验证账号密码是否正确,即是否在数据库中存在,注册过
	pass, dbErr := opdb.LoginPass(loginReq)
	if !pass {
		c.JSON(http.StatusOK, gin.H{
			"status": -1,
			"msg":    "账号或密码错误" + dbErr.Error(),
			"data":   nil,
		})
		return
	}
	//创建一个token
	token := generateToken(c, loginReq)
//创建一个token
func generateToken(c *gin.Context, loginReq dfst.LoginReq) (token string) {
	// 构造SignKey: 签名和解签名需要使用一个值
	jwt := middleware.NewJWT()
	// 构造用户claims信息(负荷)
	claims := middleware.CustomClaims{
		Name: loginReq.Name,
		StandardClaims: jwt2.StandardClaims{
			NotBefore: time.Now().Unix() - 1000, // 签名生效时间
			ExpiresAt: time.Now().Unix() + 3600, // 签名过期时间
			Issuer:    "wxf.top",                // 签名颁发者
		},
	}
	// 根据claims生成token对象
	token, err := jwt.CreateToken(claims)
	if err != nil {
		c.JSON(http.StatusOK, gin.H{
			"status": -1,
			"msg":    err.Error(),
			"data":   nil,
		})
	}
	log.Println("create token", token)
	return
}
//创建token
func (j *JWT) CreateToken(claims CustomClaims) (string, error) {
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
	//获取完整的签名令牌
	return token.SignedString(j.SigningKey)
}

校验token:

  1. 从header的头部的token字段获取token
  2. 解析token,并将PAYLOAD负载提取出来
  3. 将负载添加到context上下文中供调用链中的函数使用
func JWTAuth(c *gin.Context) {
	//从header的头部的token字段获取token
	token := c.Request.Header.Get("token")
	if token == "" {
		c.JSON(http.StatusOK, gin.H{
			"status": -1,
			"msg":    "请求未携带token,无权限访问",
			"data":   nil,
		})
		c.Abort()
		return
	}
	log.Println("recv tokens:", token)
	j := NewJWT()
	//解析token,并将PAYLOAD负载提取出来
	claims, err := j.ParserToken(token)
	if err != nil {
		// token过期
		if err == TokenExpired {
			c.JSON(http.StatusOK, gin.H{
				"status": -1,
				"msg":    "token授权已过期,请重新申请授权",
				"data":   nil,
			})
			//中断调用链
			c.Abort()
			return
		}
		// 其他错误
		c.JSON(http.StatusOK, gin.H{
			"status": -1,
			"msg":    err.Error(),
			"data":   nil,
		})
		c.Abort()
		return
	}
	//将负载添加到context上下文中供调用链中的函数使用
	c.Set("claims", claims)
}
//解析token,并将PAYLOAD负载提取出来
func (j *JWT) ParserToken(tokenString string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		return j.SigningKey, nil
	})
	if err != nil {
		// jwt.ValidationError 是一个无效token的错误结构
		if ve, ok := err.(*jwt.ValidationError); ok {
			// ValidationErrorMalformed是一个uint常量,表示token不可用
			if ve.Errors&jwt.ValidationErrorMalformed != 0 {
				return nil, TokenMalformed
				// ValidationErrorExpired表示Token过期
			} else if ve.Errors&jwt.ValidationErrorExpired != 0 {
				return nil, TokenExpired
				// ValidationErrorNotValidYet表示无效token
			} else if ve.Errors&jwt.ValidationErrorNotValidYet != 0 {
				return nil, TokenNotValidYet
			} else {
				return nil, TokenInvalid
			}
		}
	}
	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}
	return nil, TokenInvalid
}