gin-vue-admin学习(后端篇)—— 2.中间件执行原理

上一节知道了gin-vue-admin是如何进行login登录并生成token给到前端的。本节就来看看后端是如何验证token是否合规的。对于验证token,就是在后端调用处理api方法之前,统一会进行验证的地方。在gin里是通过中间件形式进行的。可以打开middleware/jwt.go文件,即为jwt中间件。

1.代码示例

func JWTAuth() gin.HandlerFunc {
	return func(c *gin.Context) {
		// 我们这里jwt鉴权取头部信息 x-token 登录时回返回token信息 这里前端需要把token存储到cookie或者本地localStorage中 不过需要跟后端协商过期时间 可以约定刷新令牌或者重新登录
		token := c.Request.Header.Get("x-token")
		if token == "" {
			response.FailWithDetailed(gin.H{"reload": true}, "未登录或非法访问", c)
			c.Abort()
			return
		}
		if jwtService.IsBlacklist(token) {
			response.FailWithDetailed(gin.H{"reload": true}, "您的帐户异地登陆或令牌失效", c)
			c.Abort()
			return
		}
		j := utils.NewJWT()
		// parseToken 解析token包含的信息
		claims, err := j.ParseToken(token)
		if err != nil {
			if err == utils.TokenExpired {
				response.FailWithDetailed(gin.H{"reload": true}, "授权已过期", c)
				c.Abort()
				return
			}
			response.FailWithDetailed(gin.H{"reload": true}, err.Error(), c)
			c.Abort()
			return
		}
    
		if claims.ExpiresAt-time.Now().Unix() < claims.BufferTime {
			claims.ExpiresAt = time.Now().Unix() + global.GVA_CONFIG.JWT.ExpiresTime
			newToken, _ := j.CreateTokenByOldToken(token, *claims)
			newClaims, _ := j.ParseToken(newToken)
			c.Header("new-token", newToken)
			c.Header("new-expires-at", strconv.FormatInt(newClaims.ExpiresAt, 10))
			if global.GVA_CONFIG.System.UseMultipoint {
				RedisJwtToken, err := jwtService.GetRedisJWT(newClaims.Username)
				if err != nil {
					global.GVA_LOG.Error("get redis jwt failed", zap.Error(err))
				} else { // 当之前的取成功时才进行拉黑操作
					_ = jwtService.JsonInBlacklist(system.JwtBlacklist{Jwt: RedisJwtToken})
				}
				// 无论如何都要记录当前的活跃状态
				_ = jwtService.SetRedisJWT(newToken, newClaims.Username)
			}
		}
		c.Set("claims", claims)
		c.Next()
	}
}

可以看出,编写中间件,只要就是编写一个gin.HandlerFunc类型的方法,在方法中会通过获取请求头中的x-token即为本次请求所发送的token。若token为空或者再黑名单中,就返回失败。然后就是对token进行解码,验证token是否符合规定。若符合规定,然后判断token有效时间是否小于缓冲时间,若小于缓冲时间,则重新生成token并返回给前端。最后,将用户的信息通过key=claims来放到context中。

可以通过打断点的方式来查看claims里边具体是哪些信息:
在这里插入图片描述

后边的api逻辑处理部分就可以通过c.Get(“claims”)来获取到对应的用户信息了。

2.执行原理

我们可以继续在这个中间件上打断点,重新启动项目,然后通过前端请求来让后端执行逻辑走到断点处,此时我们看执行堆栈:

在这里插入图片描述
查看堆栈命名可以看出,handleHTTPRequest方法是开始处理请求的。 因此可以先跳到这里查看代码。

func (engine *Engine) handleHTTPRequest(c *Context) {
	httpMethod := c.Request.Method
	rPath := c.Request.URL.Path
	unescape := false

	// Find root of the tree for the given HTTP method
	t := engine.trees
	for i, tl := 0, len(t); i < tl; i++ {
		if t[i].method != httpMethod {
			continue
		}
		root := t[i].root
		// Find route in tree
		value := root.getValue(rPath, c.params, unescape)
		if value.params != nil {
			c.Params = *value.params
		}
		if value.handlers != nil {
			c.handlers = value.handlers
			c.fullPath = value.fullPath
			c.Next()
			c.writermem.WriteHeaderNow()
			return
		}
}

我删掉了多余处理其他逻辑的代码,只保留了主逻辑部分。方法内的前两行主要是获取当前请求的方法和路由。然后for循环内只要是找到对应的方法树。对于方法树,具体可以查看engine.trees变量:

在这里插入图片描述

一共有五棵树,相当于本项目只用到了Http中的5种方法。然后对于当前请求是哪种方法,就去找出对应的树。再去该树中找到对应的节点后,将handles和fullPath赋值给context对象。只有context对象执行了Next方法,我们可以点到下一层堆栈来看下具体。

// go/pkg/mod/github.com/gin-gonic/gin@v1.7.0/context.go
func (c *Context) Next() {
	c.index++
	for c.index < int8(len(c.handlers)) {
		c.handlers[c.index](c)
		c.index++
	}
}

虽然这里的代码很短,但是涵盖了整个中间件执行的过程。首先对于c.handlers正是在上一个堆栈中,通过方法树中的对应路由的节点所配置的handles复制给c.handlers.我们可以查看当前c.handlers具体是哪些内容:

在这里插入图片描述

可以看出,一共5个handler,只有最后一个是实际的程序逻辑。在每个handler处理完成或者显示调用c.Next()后,就会继续触发下一个handler。

我们接下来可以看下对于这5个handler都做了哪些工作

首先第一个是gin.LoggerWithConfig.func1,具体代码如下:

func(c *Context) {
		// Start timer
		start := time.Now()
		path := c.Request.URL.Path
		raw := c.Request.URL.RawQuery

		// Process request
		c.Next()

		// Log only when path is not being skipped
		if _, ok := skip[path]; !ok {
			param := LogFormatterParams{
				Request: c.Request,
				isTerm:  isTerm,
				Keys:    c.Keys,
			}

			// Stop timer
			param.TimeStamp = time.Now()
			param.Latency = param.TimeStamp.Sub(start)

			param.ClientIP = c.ClientIP()
			param.Method = c.Request.Method
			param.StatusCode = c.Writer.Status()
			param.ErrorMessage = c.Errors.ByType(ErrorTypePrivate).String()

			param.BodySize = c.Writer.Size()

			if raw != "" {
				path = path + "?" + raw
			}

			param.Path = path

			fmt.Fprint(out, formatter(param))
		}

由之前阅读的代码可知,c.Next()可以触发执行下一个handler,因此可以以此为分界点。c.Next() 的上下部分是对执行c.Next()进行耗时统计。所以整体上就是对当前请求接口处理耗时进行统计。

第二个是CustomRecoveryWithWriter.func1,具体代码如下:

return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				if logger != nil {
					stack := stack(3)
					httpRequest, _ := httputil.DumpRequest(c.Request, false)
					headers := strings.Split(string(httpRequest), "\r\n")
					for idx, header := range headers {
						current := strings.Split(header, ":")
						if current[0] == "Authorization" {
							headers[idx] = current[0] + ": *"
						}
					}
					headersToStr := strings.Join(headers, "\r\n")
					if brokenPipe {
						logger.Printf("%s\n%s%s", err, headersToStr, reset)
					} else if IsDebugging() {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s",
							timeFormat(time.Now()), headersToStr, err, stack, reset)
					} else {
						logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s",
							timeFormat(time.Now()), err, stack, reset)
					}
				}
				if brokenPipe {
					// If the connection is dead, we can't write a status to it.
					c.Error(err.(error)) // nolint: errcheck
					c.Abort()
				} else {
					handle(c, err)
				}
			}
		}()
		c.Next()
	}

可以看到第二行写了个defer代表是异常后释放资源的逻辑。所以这个中间件整体上就是对请求出异常情况下,执行的资源释放等操作。通过具体阅读代码可知,里边主要是对出异常后,收集请求等信息,并打印日志。

第三个是JWT了,之前有详细讲述,这里就不在赘述了

第四个是CasbinHandler这个里边调用了Casbin库,一个用于权限管理的库,为了判别当前用户是否有权限执行此api

第五个就是我们具体业务处理的handler了。

gin框架中,对于第一和第二个是默认有的,后边的jwt和权限管理,则是项目中自己添加的中间件.

PrivateGroup.Use(middleware.JWTAuth()).Use(middleware.CasbinHandler())
  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值