手把手带你从0到1封装Gin框架:09 中间件&cors&权限验证

313 阅读6分钟

项目源码

Github

中间件是一种软件设计模式,它允许在应用程序的请求 - 响应处理流程中插入自定义的逻辑,可以用于实现日志记录、权限验证、请求限流、错误处理等功能

通过合理地运用中间件,我们可以使应用更加健壮、安全和可扩展

中间件简单使用

那在Gin框架中如何使用中间件呢?通过查看源码发现有个engine.Use方法:

// Use attaches a global middleware to the router. i.e. the middleware attached through Use() will be  
// included in the handlers chain for every single request. Even 404, 405, static files...  
// For example, this is the right place for a logger or error management middleware.
// Use 函数用于向 Gin 引擎添加全局中间件。这些中间件将被应用于所有请求,包括 404、405 和静态文件请求等。
// 例如,这是添加日志记录器或错误管理中间件的理想位置。
func (engine *Engine) Use(middleware...HandlerFunc) IRoutes {
	engine.RouterGroup.Use(middleware...)
	engine.rebuild404Handlers()
	engine.rebuild405Handlers()
	return engine
}

可以通过这个方法来添加中间件,Use 方法可以接收多个type HandlerFunc func(*Context)类型的方法,这个方法类型看着有点眼熟,其实它就是路由控制里边的 Handler,这个我们后面再说,这里先看一下路由的简单使用:

func main() {
	route := gin.Default()

	route.Use(middleware)

	route.GET("/a", func(c *gin.Context) {
		fmt.Println("handler executed")
	})

	err := route.Run(":8085")
	if err != nil {
		return
	}
}

func middleware(c *gin.Context) {
	fmt.Println("before handler")
	c.Next()
	fmt.Println("after handler")
}

上例中定义了一个中间件并通过Use方法加载到HTTP服务中,访问 /a 路由可以看到控制台有输出:

before handler
handler executed
after handler

通过输出内容也可以看到中间件和Handler的执行顺序,那我们再做一下调整:


func main() {
	route := gin.Default()

	route.GET("/a", func(c *gin.Context) {
		fmt.Println("handler a executed")
	})

	route.Use(middleware)

	route.GET("/b", func(c *gin.Context) {
		fmt.Println("handler b executed")
	})

	err := route.Run(":8085")
	if err != nil {
		return
	}
}

func middleware(c *gin.Context) {
	fmt.Println("before handler")
	c.Next()
	fmt.Println("after handler")
}

访问路由/a会发现控制台输出:

handler a executed

中间件middleware并没有被执行,再访问路由/b查看控制台输出:

before handler
handler b executed
after handler

这里中间件方法被执行了,这说明中间件的执行顺序是跟它的注册顺序有关的

那如果是多个中间件呢?再做一下调整:

func main() {
	route := gin.Default()

	route.GET("/a", func(c *gin.Context) {
		fmt.Println("handler a executed")
	})

	route.Use(middleware1, middleware2)

	route.GET("/b", func(c *gin.Context) {
		fmt.Println("handler b executed")
	})

	err := route.Run(":8085")
	if err != nil {
		return
	}
}

func middleware1(c *gin.Context) {
	fmt.Println("middleware 1 before handler")
	c.Next()
	fmt.Println("middleware 1 after handler")
}

func middleware2(c *gin.Context) {
	fmt.Println("middleware 2 before handler")
	c.Next()
	fmt.Println("middleware 2 after handler")
}

访问/b路由,查看控制台输出:

middleware 1 before handler
middleware 2 before handler
handler b executed
middleware 2 after handler
middleware 1 after handler

可以看到先注册的中间件是先执行的,并且在调用 c.Next() 方法之后会执行后边的中间件,然后再执行 c.Next 后边的内容

局部使用中间件

到这里我们会发现一个问题,在中间件注册之前的Handler 都不受中间件的影响,在注册之后的Handler 都会执行中间件,那如果我们只想在一个路由或者一组路由 中使用某个中间件,其他地方不使用呢?如果仅仅通过路由和中间件的注册顺序去控制,会很麻烦,好在Gin 框架也支持,看源码:

// Use adds middleware to the group, see example code in GitHub.
func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
	group.Handlers = append(group.Handlers, middleware...)
	return group.returnObj()
}

可以对我们之前使用的RouterGroup通过Use 方法来注册中间件,并且只对 group 内部的路由生效,哪怕在 group 之后注册的路由也不会生效,调整一下代码:


func main() {
	route := gin.Default()

	rg := route.Group("/admin").Use(middleware1)
	rg.GET("/a", func(c *gin.Context) {
		fmt.Println("handler a executed")
	})

	route.Use(middleware2)

	route.GET("/b", func(c *gin.Context) {
		fmt.Println("handler b executed")
	})

	err := route.Run(":8085")
	if err != nil {
		return
	}
}

func middleware1(c *gin.Context) {
	fmt.Println("middleware 1 before handler")
	c.Next()
	fmt.Println("middleware 1 after handler")
}

func middleware2(c *gin.Context) {
	fmt.Println("middleware 2 before handler")
	c.Next()
	fmt.Println("middleware 2 after handler")
}

分别访问路由/admin/a/b,查看控制台输出:

/admin/a
middleware 1 before handler
handler a executed
middleware 1 after handler

/b
middleware 2 before handler
handler b executed
middleware 2 after handler

可以看到 group 注册的中间件 middleware1 对路由/b不生效

单个路由注册中间件

那如果有需要让我们只给某一个路由注册中间件的时候,怎么办?单独搞一个 group 出来?也不是不可以,但是有更简单的方法,前边我们提到中间件其实是一个 type HandlerFunc func(*Context) 类型的方法,而路由的 Handler 其实也是一样的类型,所谓中间件其实也是一个Handler,那我们在注册路由时直接加上中间件就可以了,修改代码:

func main() {
	route := gin.Default()

	route.GET("/a", middleware1, func(c *gin.Context) {
		fmt.Println("handler a executed")
	})

	route.Use(middleware2)

	route.GET("/b", func(c *gin.Context) {
		fmt.Println("handler b executed")
	})

	err := route.Run(":8085")
	if err != nil {
		return
	}
}

func middleware1(c *gin.Context) {
	fmt.Println("middleware 1 before handler")
	c.Next()
	fmt.Println("middleware 1 after handler")
}

func middleware2(c *gin.Context) {
	fmt.Println("middleware 2 before handler")
	c.Next()
	fmt.Println("middleware 2 after handler")
}

然后分别访问路由/a/b,查看控制台:

/a 
middleware 1 before handler
handler a executed
middleware 1 after handler

/b
middleware 2 before handler
handler b executed
middleware 2 after handler

可以看到这种形式也可以注册中间件,那下边我们在项目中使用中间件

cors中间件

作为一个web项目,对外提供接口是最基本的功能了,跨域也是很寻常的需求,所以这里我们加一个跨域中间件

新增app/middleware/cors.go文件:

package middleware

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

func CorsMiddleware(c *gin.Context) {
	c.Header("Access-Control-Allow-Origin", "*")
	c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
	c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization")

	if c.Request.Method == "OPTIONS" {
		c.AbortWithStatus(http.StatusNoContent)
		return
	}

	c.Next()
}

再修改app/route/admin.go文件:

package route

import (
	"eve/app/api/admin"
	"eve/app/middleware"
	"github.com/gin-gonic/gin"
)

func genAdminRouter(rg *gin.RouterGroup) {
	rg.Use(middleware.CorsMiddleware)
	rg.GET("/profile", admin.AdminApi.Profile)
	rg.POST("/save", admin.AdminApi.Save)
}

这样cors中间件也就注册好了

权限验证中间件

新增文件:

package middleware

import (
	"eve/app/response"
	"github.com/gin-gonic/gin"
)

func PermissionMiddleware(c *gin.Context) {
	// 获取路由
	//path := c.FullPath()
	// 使用路由和当前登陆用户去判断是否有当前接口的访问权限
	if isHasPermission() {
		c.Next()
	} else {
		response.PermissionDenied(c)
		c.Abort()
	}

}

// 权限验证,这里根据自己的业务逻辑去实现
func isHasPermission() bool {
	return true
}

修改app/route/admin.go文件:

package route

import (
	"eve/app/api/admin"
	"eve/app/middleware"
	"github.com/gin-gonic/gin"
)

func genAdminRouter(rg *gin.RouterGroup) {
	rg.Use(middleware.CorsMiddleware)

	// 登陆接口,
	rg.POST("/login", func(c *gin.Context) {
		c.JSON(200, gin.H{"message": "login"})
	})

	// ------ 以上的接口不走权限校验 ------

	// 注册权限验证中间件
	rg.Use(middleware.PermissionMiddleware)

	rg.GET("/profile", admin.AdminApi.Profile)
	rg.POST("/save", admin.AdminApi.Save)
}

完善response反回信息,新增app/response/error.go文件:

package response

const (
	SystemError = iota + 400000
	BusinessError
	HaveNoPermission
	ValidateError
)

var ErrorMap = map[int]string{
	SystemError:      "系统错误",
	BusinessError:    "业务错误",
	HaveNoPermission: "没有操作权限",
	ValidateError:    "数据校验失败",
}

修改app/response/response.go文件:

package response

import (
	"github.com/gin-gonic/gin"
	"net/http"
)

// Response 定义一个统一的返回值结构体
type Response struct {
	ErrorCode int         `json:"error_code"`
	Data      interface{} `json:"data"`
	Message   string      `json:"message"`
}

// Success 成功返回
func Success(c *gin.Context, data interface{}) {
	c.JSON(http.StatusOK, Response{
		0,
		data,
		"ok",
	})
}

func ValidateFailed(c *gin.Context, msg string) {
	Error(c, ValidateError, msg)
}

func PermissionDenied(c *gin.Context) {
	Error(c, HaveNoPermission, ErrorMap[HaveNoPermission])
}

func BusinessFail(c *gin.Context, msg string) {
	Error(c, BusinessError, msg)
}

// Error 失败返回
func Error(c *gin.Context, errorCode int, msg string) {
	c.JSON(http.StatusOK, Response{
		errorCode,
		nil,
		msg,
	})
}

中间件已经添加,后续的业务中只需要实现PermissionMiddleware中的isHasPermission方法即可

总结

  • 中间件的简单使用
  • 中间件的执行顺序
  • 局部中间件与全局中间件
  • 反回信息重构

commit-hash: a424c4d