Gee教程5.中间件

鉴权认证、日志记录等这些保障和支持系统业务属于全系统的业务,和具体的系统业务没有关联,对于系统中的很多业务都适用。

因此,在业务开发过程中,为了更好的梳理系统架构,可以将上述描述所涉及的一些通用业务单独抽离并进行开发,然后以插件化的形式进行对接。这种方式既保证了系统功能的完整,同时又有效地将具体业务和系统功能进行解耦,还可以达到灵活配置的目的。

这种通用业务独立开发并灵活配置使用的组件,一般称之为"中间件"。其就相当于在请求和具体的业务逻辑处理之间增加某些操作,这种以额外添加的方式不会影响编码效率,也不会侵入到框架中。

每个用户都有自己的业务,Web框架不可能实现所有的功能。因此,框架需要有一个插口,允许用户自己定义通用功能,嵌入到框架中,仿佛这个功能是框架原生支持的一样。这个也就是我们所说的中间件。

通俗点理解,中间件就是个函数。比如前缀是/admin来访问的都需要进行鉴权认证,而Web框架又没有实现这个功能。

这时可能想到直接在该路由的处理函数加上这个功能就行啦。但是要是有很多前缀是/admin的路由,那就要写很多很多,这是很繁琐的。

中间件的使用

那我们就想有这种效果,这种就写的很舒服了,gin框架就是这样使用的。

//鉴权中间件
func AuthMiddleWare() gee.HandlerFunc {
	........
}
 
func main() {
	r := gee.New()
	v2 := r.Group("/admin")	
	v2.Use(AuthMiddleWare()) //该路由组使用鉴权中间件,也只有该组使用这个中间件
	v2.GET("/home", func(c *gee.Context) {
        //AuthMiddleWare()
		c.JSON(http.StatusOK, gin.H{
			"msg": "home路由",
		})
	})
	v2.GET("/ok", func(c *gee.Context) {
        //AuthMiddleWare()
		c.JSON(http.StatusOK, gin.H{
			"msg": "ok路由",
		})
	})
}

中间件的设计

前面说了中间件是个函数,那可以设置成映射的 Handler 一致,处理的输入是Context对象。

type HandlerFunc func(*Context)

一个路由组可以会使用多个中间件的,上一节中已在路由组的结构中添加了中间件数组。那来实现下添加中间件方法Use。该方法可以一次添加多个中间件。

RouterGroup struct {
	prefix      string
	middlewares []HandlerFunc // support middleware
	parent      *RouterGroup  // support nesting
	engine      *Engine       // all groups share a Engine instance
}

func (group *RouterGroup) Use(middlewares ...HandlerFunc) {
	group.middlewares = append(group.middlewares, middlewares...)
}

简单想法:一条直线处理

我们想的简单的版本,前后添加了中间件A,中间件B。那处理的流程就是:中间件A -----> 中间件B -----> 路由处理函数。

回到我们的执行路由处理函数部分,那么ServeHTTP就需要改动下。

//之前的
//func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
//	c := newContext(w, req)
//	engine.router.handle(c)
//}

//需要执行中间件的做法
func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	c := newContext(w, req)
	for _, group := range engine.groups {
		if strings.HasPrefix(req.URL.Path, group.prefix) {
			group.middlewares(c)    //执行中间件
		}
	}

	engine.router.handle(c)
}

这种实现起来是很简单,但却不实用的。就像是要鉴权认证,假如认证失败后,那肯定是不执行路由处理函数的,而这个做法却是继续执行的。

还有有些中间件想在路由处理函数执行后做一些操作,例如计算响应时长之类的。 那就要等路由处理函数执行后才可以计算,当前实现的做法是不妥的,不能实现这样功能的。所以需要继续改善。

我们想要的中间件是支持用户在请求被处理的前后,做一些额外的操作

改善

现在来看一个中间件,日志中间件。

假如我们要计算响应时长,那就是要等路由Handler处理完之后才能执行第八行代码。所以需要做些操作。

那在第六行中,c.Next()表示执行其他的中间件或用户的Handler。

等执行完其他中间件和用户的Handler后,再执行第八行代码,这样就可以符合我们想记录响应时长的要求了。

func Logger() HandlerFunc {
	return func(c *Context) {
		// Start timer
		t := time.Now()
		// Process request
		c.Next()
		// Calculate resolution time
		log.Printf("[%d] %s in %v", c.StatusCode, c.Req.RequestURI, time.Since(t))
	}
}

//鉴权中间件
func AuthMiddleWare() HandlerFunc {
	........
}
func main() {
	r := gee.New()    
    r.Use(Logger())
	v2 := r.Group("/admin")	
	v2.Use(AuthMiddleWare()) //该路由组使用鉴权中间件,也只有该组使用这个中间件
	v2.GET("/home", func(c *gee.Context) {
		c.JSON(http.StatusOK, gin.H{
			"msg": "home路由",
		})
	})

}

 那么重点就是在c.Next()中了。c.Next()可以执行中间件,那说明Context保存了该中间件。所以,Context结构体添加了中间件切片midHandlers和index。

type Context struct {
	// origin objects
	Writer http.ResponseWriter
	Req    *http.Request
	// request info
	Path   string
	Method string
	Params map[string]string
	// response info
	StatusCode int
	// middleware
	midHandlers []HandlerFunc        //这节新添加的
	index    int8        //这节新添加的
}    

func newContext(w http.ResponseWriter, req *http.Request) *Context {
	return &Context{
		Wrtier: w,
		Req:    req,
		Path:   req.URL.Path,
		Method: req.Method,
		index:  -1, //这节新添加的
	}
}

func (c *Context) Next() {
	c.index++

	for c.index < int8(len(c.midHandlers)) {
		c.midHandlers[c.index](c) //执行中间件
		c.index++
	}
}

index是记录当前执行到第几个中间件,当在中间件中调用Next方法时,控制权交给了下一个中间件,直到调用到最后一个中间件,然后再从后往前,调用每个中间件在Next方法之后定义的部分。

如果我们将用户在映射路由时定义的Handler添加到c.handlers列表中,结果会怎么样呢?

func A(c *Context) {
    part1        //可以是记录开始时间
    c.Next()
    part2        //可以是记录整个响应时长
}
func B(c *Context) {
    part3
    c.Next()
    part4
}

假设我们应用了中间件 A 和 B,和路由映射的 Handler。那么c.handlers是这样的:[A, B, Handler],c.index初始化为-1。

先执行中间件A,那就是先执行到part1。之后到c.Next(),这时候就到执行c.handlers中的下一个,即是中间件B,那就是执行part3。之后执行c.Next(),这时就执行到路由Hander,跟着就到了part4。part4结束后,即是中间件中的c.Next()结束了,最终执行part2.

index是怎么变化的要到后面才好说明白。

最终的顺序是part1 -> part3 -> Handler -> part 4 -> part2。恰恰满足了我们对中间件的要求。

接着就看回到ServeHTTP方法。

func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) {
	// c := newContext(w, req)
	// engine.router.handle(c)

	var middlewares []HandlerFunc
	for _, group := range engine.gorups {
		if strings.HasPrefix(req.URL.Path, group.prefix) {
			middlewares = append(middlewares, group.middlewares...) //添加该路由组的中间件
		}
	}
	c := newContext(w, req)
	c.midHandlers = middlewares    //路由组中间件都赋值给Context结构体的midHandlers 
	engine.router.handle(c)
}

那之后肯定是在engine.router.handle(c)中有些变化的了。

最后的c.Next()是很重要的,这样操作后,所有的中间件和路由Handler都统一在Next()中执行的了。

这里可以把之前例子里的index的疑惑讲明白了。c.index默认初始化是-1的,这里执行C.Next(),c.index就变成0了,就可以执行c.handler[0],即是中间件A。

func (r *router) handle(c *Context) {
	n, params := r.getRoute(c.Method, c.Path)
	if n != nil {
		key := c.Method + "-" + n.path
		c.Params = params
		c.midHandlers = append(c.midHandlers, r.handers[key]) //添加路由Handler到midHandlers中
	} else {
		c.midHandlers = append(c.midHandlers, func(c *Context) {
			c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
		})
	}
	c.Next() //从这里开始执行第一个中间件,要是没有中间件就直接执行路由Handler


    //上一节的做法
	//n, params := r.getRoute(c.Method, c.Path)
	// if n != nil {
	// 	c.Params = params
	// 	// key := c.Method + "-" + c.Path
	// 	key := c.Method + "-" + n.path
	// 	fmt.Println(key)
	// 	r.handers[key](c)
	// } else {
	// 	c.String(http.StatusNotFound, "404 NOT FOUND: %s\n", c.Path)
	// }
}

 说完了c.Next()后,还有c.Abort()方法。

在进行鉴权认证不通过后,后面的中间件和路由Handler也就不需要执行的了。

在c.Next()中是通过c.index来进行判断执行哪个中间件的。首先我们定义AbortIndex常量,主要是用于控制框架处理请求链路的长度,即是中间件和路由Handler总个数。这时候设置c.index=AbortIndex,那么c.Next()就不会执行中间件了。那么我们在之前使用Use添加中间件的时候,就需要判断中间件个数是否超过了最大个数,这里就不展示了。

const AbortIndex = math.MaxInt8 >> 1

func (c *Context) Abort() {
	c.index = AbortIndex
}

测试

场景:/admin的使用鉴权认证中间件,全局的使用日志中间件。

鉴权失败的话,调用c.Abort()

func authMiddleWare() gee.HandlerFunc {
	return func(c *gee.Context) {
		fmt.Println("start 鉴权中间件")
		token := c.Req.Header.Get("token")
		if token == "" || token != "123" {
			c.JSON(http.StatusUnauthorized, gee.H{"message": "身份验证失败"})
			c.Abort()
			//return //不使用return的话,后面的fmt.Println("hello")会执行,使用return的话,后面的不会执行
		} else {
			fmt.Println("鉴权成功")
		}
		//使用Abort()后要是不使用return,打印hello还是会执行的
		//fmt.Println("hello")
	}
}

func main() {
	r := gee.New()
	r.Use(gee.Logger())
	r.GET("/home", func(c *gee.Context) {
		c.HTML(http.StatusOK, "<h1>Index home</h1>")
	})

	v1 := r.Group("/admin")
	v1.Use(authMiddleWare())
	{
		v1.GET("/myname", func(c *gee.Context) {
			c.String(http.StatusOK, "Hello li")
		})
	}

	r.Run("localhost:10000")
}

结果

完整代码:https://github.com/liwook/Go-projects/tree/main/gee-web/5-Middlewares

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/209270.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯第198题 人物相关性分析 C++ 模拟 字符串 双指针

题目 思路和解题方法 程序首先定义了一个函数check&#xff0c;用于判断一个字符是否为字母。接下来&#xff0c;程序读取输入的整数k和一行字符串str。定义了两个空的向量a和b&#xff0c;用于存储满足条件的子串的起始位置。使用for循环遍历字符串str的每个字符&#xff0c;检…

Python--使用布林线设计均值回归策略

在本教程中,我们将探讨均值回归的概念以及如何使用 Python 中的布林线设计交易策略。均值回归是一种流行的交易策略,它基于这样的假设:随着时间的推移,资产价格往往会恢复到历史平均水平。布林线 (Bollinger Bands) 由约翰布林格 (John Bollinger) 开发,是一种技术分析工具…

喜讯 | Circulation(IF:37.8)ChIP-seq+RNA-seq助力解析USP28在糖尿病性心脏病的调控机制

2023年11月23日&#xff0c;国际知名期刊Circulation&#xff08;IF:37.8&&#xff09;在线发表了武汉大学人民医院心内科唐其柱教授团队题为 ” USP28 Serves as a Key Suppressor of Mitochondrial Morphofunctional Defects and Cardiac Dysfunction in the Diabetic He…

OSI七层模型与TCP/IP四层模型

一、OSI七层模型简述 OSI 模型的七层是什么&#xff1f;在 OSI 模型中如何进行通信&#xff1f;OSI 模型有哪些替代方案&#xff1f; TCP/IP 模型关于专有协议和模型的说明 二、七层模型详解&#xff08;DNS、CDN、OSI&#xff09; 状态码DNS nslookup命令 CDN whois命令 …

熬夜会秃头——beta冲刺Day4

这个作业属于哪个课程2301-计算机学院-软件工程社区-CSDN社区云这个作业要求在哪里团队作业—beta冲刺事后诸葛亮-CSDN社区这个作业的目标记录beta冲刺Day4团队名称熬夜会秃头团队置顶集合随笔链接熬夜会秃头——Beta冲刺置顶随笔-CSDN社区 一、团队成员会议总结 1、成员工作进…

计算机网络——数据链路层-封装成帧(帧定界、透明传输-字节填充,比特填充、MTU)

目录 介绍 帧定界 PPP帧 以太网帧 透明传输 字节填充&#xff08;字符填充&#xff09; 比特填充 比特填充习题 MTU 介绍 所谓封装成帧&#xff0c;就是指数据链路层给上层交付下来的协议数据单元添加帧头和帧尾&#xff0c;使之成为帧。 例如下图所示&#xff1a; …

【LeetCode刷题笔记】102. 二叉树的层序遍历

创作不易&#xff0c;本篇文章如果帮助到了你&#xff0c;还请点赞 关注支持一下♡>&#x16966;<)!! 主页专栏有更多知识&#xff0c;如有疑问欢迎大家指正讨论&#xff0c;共同进步&#xff01; 更多算法知识专栏&#xff1a;算法分析&#x1f525; 给大家跳段街舞感谢…

NRF24L01 无线收发模块与 Arduino 的应用

NRF24L01 是一款常用的无线收发模块&#xff0c;与 Arduino 兼容性良好&#xff0c;可以用于实现无线通信和数据传输。本文将介绍如何将 NRF24L01 模块与 Arduino 配合使用&#xff0c;包括硬件的连接和配置&#xff0c;以及相应的代码示例。 一、引言 NRF24L01 是一款基于 2.…

图解「差分」入门(“前缀和“ 到 “差分“ 丝滑过渡)

题目描述 这是 LeetCode 上的 「1094. 拼车」 &#xff0c;难度为 「中等」。 Tag : 「差分」、「前缀和」 车上最初有 capacity 个空座位&#xff0c;车只能向一个方向行驶&#xff08;不允许掉头或改变方向&#xff09;。 给定整数 capacity 和一个数组 trips, 表示第 i 次旅…

HarmonyOs 4 (三) ArkTS语言

目录 一 认识ArkTs语言1.1 ArkTs1.2 基本结构 二 基本语法2.1 声明式UI2.1.1 创建组件2.1.1.1 无参数2.1.1.2 有参数2.1.1.3 组件样式2.1.1.4 组件方法2.1.1.5 组件嵌套 2.1.2 自定义组件2.1.2.1 基本结构2.1.2.2 成员函数/变量2.1.2.3 自定义组件的参数规定2.1.2.4 Build函数2…

最新版dede织梦CMS采集侠工具

在网络时代&#xff0c;拥有一个充实而有吸引力的网站是吸引访客、提高流量的关键。织梦CMS一直以其简单易用、灵活性强而备受欢迎。而如何让网站内容更富有吸引力&#xff0c;如何在激烈的网络竞争中脱颖而出&#xff1f;新版织梦采集侠以及147SEO插件或许是你的得力助手。 新…

创始人于东来:胖东来员工不想上班,请假不允许不批假!

12月2日早晨&#xff0c;一则关于“胖东来员工不想上班请假不允许不批假”的新闻登上了热搜&#xff0c;引起了广泛关注。熟悉胖东来的网友们可能知道&#xff0c;这并不是这家企业第一次成为热搜的焦点。据白鹿视频12月1日报道&#xff0c;11月25日&#xff0c;河南许昌的胖东…

水果软件FL Studio最新21.1.1.3750汉化版

FL Studio水果音乐编曲软件中文版,一款强大的音乐制作软件,可以进行音乐编曲、剪辑、录音、混音。FL Studio21.1.1.3750中文版是数字音频工作站 (DAW) 之一&#xff0c;日新月异。它是一款录音机和编辑器&#xff0c;可让您不惜一切代价制作精美的音乐作品并保存精彩的活动画廊…

Beta冲刺总结随笔

这个作业属于哪个课程软件工程A这个作业要求在哪里beta冲刺事后诸葛亮作业目标Beta冲刺总结随笔团队名称橘色肥猫团队置顶集合随笔链接Beta冲刺笔记-置顶-橘色肥猫-CSDN博客 文章目录 一、Beta冲刺完成情况二、改进计划完成情况2.1 需要改进的团队分工2.2 需要改进的工具流程 三…

vmware虚拟机怎么安装linux-rocky操作系统

vmware虚拟机安装linux-rocky操作系统 rocky下载地址&#xff1a;https://rockylinux.org/zh_CN/download/ 我下载boot版本&#xff0c;安装时候需要联网。 接下来一路下一步&#xff0c;硬盘这里可以选择“将虚拟磁盘存储为单个文件”&#xff0c;然后一直点击到完成就可以。…

在 SQL Server 中备份和恢复数据库的最佳方法

在SQL Server中&#xff0c;创建备份和执行还原操作对于确保数据完整性、灾难恢复和数据库维护至关重要。以下是备份和恢复过程的概述&#xff1a; 方法 1. 使用 SQL Server Management Studio (SSMS) 备份和还原数据库 按照 SSMS 步骤备份 SQL 数据库 打开 SSMS 并连接到您…

Python 如何判断一个数组中的任意元素是否出现在另外一个数组中

需求 数组1&#xff1a;[双十一,工业机器人,智能物流,机器人概念,智慧停车,新能源汽车,智能制造] 数组2 &#xff1a;[专用设备,电力设备,化学制药,智能物流] 代码&#xff1a; def ExistsArray(sArray, dArray):result False;for item in sArray:if item in dArray:result …

CGAL的四叉树、八叉树、正交树

四叉树&#xff08;Quadtree&#xff09;&#xff1a;四叉树是一种用于二维空间分割的数据结构。它将一个二维区域划分为四个象限&#xff0c;每个象限进一步细分为四个小块&#xff0c;以此类推。四叉树可以用于空间索引、图形学、地理信息系统&#xff08;GIS&#xff09;等领…

uniapp uview u-input在app(运行在安卓基座上)上不能动态控制type类型(显隐密码)

开发密码显隐功能时&#xff0c;在浏览器h5上功能是没问题的 <view class"login-item-input"><u-input:type"showPassWord ? password : text"style"background: #ecf0f8"placeholder"请输入密码"border"surround&quo…

MySQL- CRUD-单表查询

一、INSERT 添加 公式 INSERT INTO table_name [(column [, column...])] VALUES (value [, value...]); 示例&#xff1a; CREATE TABLE goods (id INT ,good_name VARCHAR(10),price DOUBLE ); #添加数据 INSERT INTO goods (id,good_name,price ) VALUES (20,华为手机,…