GoFrame 路由管理-中間件/攔截器

2022-04-14 09:22 更新

?GoFrame?提供了優(yōu)雅的中間件請(qǐng)求控制方式,該方式也是主流的?WebServer?提供的請(qǐng)求流程控制方式,基于中間件設(shè)計(jì)可以為?WebServer?提供更靈活強(qiáng)大的插件機(jī)制。經(jīng)典的中間件洋蔥模型:


中間件定義

中間件的定義和普通HTTP執(zhí)行方法?HandlerFunc?一樣,但是可以在?Request?參數(shù)中使用?Middleware?屬性對(duì)象來控制請(qǐng)求流程。

我們拿一個(gè)跨域請(qǐng)求的中間件定義來示例說明一下:

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

可以看到在該中間件中執(zhí)行完成跨域請(qǐng)求處理的邏輯后,使用?r.Middleware.Next()?方法進(jìn)一步執(zhí)行下一個(gè)流程;如果這個(gè)時(shí)候直接退出不調(diào)用?r.Middleware.Next()?方法的話,將會(huì)退出后續(xù)的執(zhí)行流程(例如可以用于請(qǐng)求的鑒權(quán)處理)。

中間件類型

中間件的類型分為兩種:前置中間件和后置中間件。前置即在路由服務(wù)函數(shù)調(diào)用之前調(diào)用,后置即在其后調(diào)用。

前置中間件

其定義類似于:

func Middleware(r *ghttp.Request) {
	// 中間件處理邏輯
	r.Middleware.Next()
}

后置中間件

其定義類似于:

func Middleware(r *ghttp.Request) {
	r.Middleware.Next()
	// 中間件處理邏輯
}

中間件注冊(cè)

中間件的注冊(cè)有多種方式,參考接口文檔: https://pkg.go.dev/github.com/gogf/gf/v2/net/ghttp

全局中間件

func (s *Server) Use(handlers ...HandlerFunc)

全局中間件是可以獨(dú)立使用的請(qǐng)求攔截方法,通過路由規(guī)則的方式進(jìn)行注冊(cè),綁定到?Server?上,由于中間件需要執(zhí)行請(qǐng)求攔截操作,因此往往是使用"模糊匹配"或者"命名匹配"規(guī)則。

全局中間件僅對(duì)動(dòng)態(tài)請(qǐng)求攔截有效,無法攔截靜態(tài)文件請(qǐng)求。

分組路由中間件

func (g *RouterGroup) Middleware(handlers ...HandlerFunc) *RouterGroup

分組路由中注冊(cè)的中間件綁定到當(dāng)前分組路由中的所有的服務(wù)請(qǐng)求上,當(dāng)服務(wù)請(qǐng)求被執(zhí)行前會(huì)調(diào)用到其綁定的中間件方法。 分組路由僅有一個(gè)?Middleware?的中間件注冊(cè)方法。分組路由中間件與全局中間件不同之處在于,分組路由中間件無法獨(dú)立使用,必須在分組路由注冊(cè)中使用,并且綁定到當(dāng)前分組路由中所有的路由上作為路由方法的一部分。

執(zhí)行優(yōu)先級(jí)

全局中間件

由于全局中間件也是通過路由規(guī)則執(zhí)行,那么也會(huì)存在執(zhí)行優(yōu)先級(jí):

  1. 首先,由于全局中間件是基于模糊路由匹配,因此當(dāng)同一個(gè)路由匹配到多個(gè)中間件時(shí),會(huì)按照路由的深度優(yōu)先規(guī)則執(zhí)行,具體請(qǐng)查看路由章節(jié);
  2. 其次,同一個(gè)路由規(guī)則下,會(huì)按照中間件的注冊(cè)先后順序執(zhí)行,中間件的注冊(cè)方法也支持同時(shí)按照先后順序注冊(cè)多個(gè)中間件;
  3. 最后,為避免優(yōu)先級(jí)混淆和后續(xù)管理,建議將所有中間件放到同一個(gè)地方進(jìn)行先后順序注冊(cè)來控制執(zhí)行優(yōu)先級(jí);

這里的建議來參考于?gRPC?的攔截器設(shè)計(jì),沒有過多的路由控制,僅在一個(gè)地方同一個(gè)方法統(tǒng)一注冊(cè)。往往越簡(jiǎn)單,越容易理解,也便于長(zhǎng)期維護(hù)。

分組路由中間件

分組路由中間件是綁定到分組路由上的服務(wù)方法,不存在路由規(guī)則匹配,因此只會(huì)按照注冊(cè)的先后順序執(zhí)行。

中間件示例

示例1,允許跨域請(qǐng)求

第一個(gè)例子,也是比較常見的功能需求。

我們需要在所有API請(qǐng)求之前增加允許跨域請(qǐng)求的返回?Header?信息,該功能可以通過中間件實(shí)現(xiàn):

package main

import (
	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS)
		group.ALL("/user/list", func(r *ghttp.Request) {
			r.Response.Writeln("list")
		})
	})
	s.SetPort(8199)
	s.Run()
}

執(zhí)行后,終端打印出路由表信息如下:

SERVER  | DOMAIN  | ADDRESS | METHOD |       ROUTE       |      HANDLER      |     MIDDLEWARE
|---------|---------|---------|--------|-------------------|-------------------|---------------------|
  default | default | :8199   | ALL    | /api.v2/user/list | main.main.func1.1 | main.MiddlewareCORS
|---------|---------|---------|--------|-------------------|-------------------|---------------------|

這里我們使用?group.Middleware(MiddlewareCORS)?將跨域中間件通過分組路由的形式注冊(cè)綁定到了?/api.v2?路由下所有的服務(wù)函數(shù)中。隨后我們可以通過請(qǐng)求 http://127.0.0.1:8199/api.v2/user/list 來查看允許跨域請(qǐng)求的?Header?信息是否有返回。


示例2,請(qǐng)求鑒權(quán)處理

我們?cè)诳缬蛘?qǐng)求中間件的基礎(chǔ)之上加上鑒權(quán)中間件。

為了簡(jiǎn)化示例,在該示例中,當(dāng)請(qǐng)求帶有?token?參數(shù),并且參數(shù)值為?123456?時(shí)可以通過鑒權(quán),并且允許跨域請(qǐng)求,執(zhí)行請(qǐng)求方法;否則返回?403 Forbidden?狀態(tài)碼。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Response.Writeln("auth")
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.Writeln("cors")
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func main() {
	s := g.Server()
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareCORS, MiddlewareAuth)
		group.ALL("/user/list", func(r *ghttp.Request) {
			r.Response.Writeln("list")
		})
	})
	s.SetPort(8199)
	s.Run()
}

執(zhí)行后,終端打印出路由表信息如下:

SERVER  | DOMAIN  | ADDRESS | METHOD |       ROUTE       |      HANDLER      |               MIDDLEWARE
|---------|---------|---------|--------|-------------------|-------------------|-----------------------------------------|
  default | default | :8199   | ALL    | /api.v2/user/list | main.main.func1.1 | main.MiddlewareCORS,main.MiddlewareAuth
|---------|---------|---------|--------|-------------------|-------------------|-----------------------------------------|

可以看到,我們的服務(wù)方法綁定了兩個(gè)中間件,跨域中間件和而鑒權(quán)中間件。 請(qǐng)求時(shí)將會(huì)按照中間件注冊(cè)的先后順序,先執(zhí)行?MiddlewareCORS?全局中間件,再執(zhí)行?MiddlewareAuth?分組中間件。 隨后我們可以通過請(qǐng)求 http://127.0.0.1:8199/api.v2/user/list 和  http://127.0.0.1:8199/api.v2/user/list?token=123456 對(duì)比來查看效果。



示例3,鑒權(quán)例外處理

使用分組路由中間件可以很方便地添加鑒權(quán)例外,因?yàn)橹挥挟?dāng)前分組路由下注冊(cè)的服務(wù)方法才會(huì)綁定并執(zhí)行鑒權(quán)中間件,否則并不會(huì)執(zhí)行到鑒權(quán)中間件。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func main() {
	s := g.Server()
	s.Group("/admin", func(group *ghttp.RouterGroup) {
		group.ALL("/login", func(r *ghttp.Request) {
			r.Response.Writeln("login")
		})
		group.Group("/", func(group *ghttp.RouterGroup) {
			group.Middleware(MiddlewareAuth)
			group.ALL("/dashboard", func(r *ghttp.Request) {
				r.Response.Writeln("dashboard")
			})
		})
	})
	s.SetPort(8199)
	s.Run()
}

執(zhí)行后,終端打印出路由表信息如下:

SERVER  | ADDRESS | DOMAIN  | METHOD | P |      ROUTE       |       HANDLER       |     MIDDLEWARE
|---------|---------|---------|--------|---|------------------|---------------------|---------------------|
  default |  :8199  | default |  ALL   | 2 | /admin/dashboard | main.main.func1.2.1 | main.MiddlewareAuth
|---------|---------|---------|--------|---|------------------|---------------------|---------------------|
  default |  :8199  | default |  ALL   | 2 | /admin/login     | main.main.func1.1   |
|---------|---------|---------|--------|---|------------------|---------------------|---------------------|

可以看到,只有?/admin/dashboard?路由的服務(wù)方法綁定了鑒權(quán)中間件?main.MiddlewareAuth?,而?/admin/login?路由的服務(wù)方法并沒有添加鑒權(quán)處理。 隨后我們?cè)L問以下URL查看效果: 

  1. http://127.0.0.1:8199/admin/login
  2. http://127.0.0.1:8199/admin/dashboard
  3. http://127.0.0.1:8199/admin/dashboard?token=123456


示例4,統(tǒng)一的錯(cuò)誤處理

基于中間件,我們可以在服務(wù)函數(shù)執(zhí)行完成后做一些后置判斷的工作,特別是統(tǒng)一數(shù)據(jù)格式返回、結(jié)果處理、錯(cuò)誤判斷等等。這種需求我們可以使用后置的中間件類型來實(shí)現(xiàn)。我們使用一個(gè)簡(jiǎn)單的例子,用來演示如何使用中間件對(duì)所有的接口請(qǐng)求做后置判斷處理,作為一個(gè)拋磚引玉作用。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func MiddlewareErrorHandler(r *ghttp.Request) {
	r.Middleware.Next()
	if r.Response.Status >= http.StatusInternalServerError {
		r.Response.ClearBuffer()
		r.Response.Writeln("哎喲我去,服務(wù)器居然開小差了,請(qǐng)稍后再試吧!")
	}
}

func main() {
	s := g.Server()
	s.Use(MiddlewareCORS)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareAuth, MiddlewareErrorHandler)
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("db error: sql is xxxxxxx")
		})
	})
	s.SetPort(8199)
	s.Run()
}

執(zhí)行后,終端打印出路由表信息如下:

SERVER  | DOMAIN  | ADDRESS | METHOD |       ROUTE       |       HANDLER       |                   MIDDLEWARE
|---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------|
  default | default | :8199   | ALL    | /*                | main.MiddlewareCORS | GLOBAL MIDDLEWARE
|---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------|
  default | default | :8199   | ALL    | /api.v2/user/list | main.main.func1.1   | main.MiddlewareAuth,main.MiddlewareErrorHandler
|---------|---------|---------|--------|-------------------|---------------------|-------------------------------------------------|

在該示例中,我們?cè)诤笾弥虚g件中判斷有無系統(tǒng)錯(cuò)誤,如果有則返回固定的提示信息,而不是把敏感的報(bào)錯(cuò)信息展示給用戶。當(dāng)然,在真實(shí)的項(xiàng)目場(chǎng)景中,往往還有是需要解析返回緩沖區(qū)的數(shù)據(jù),例如?JSON?數(shù)據(jù),根據(jù)當(dāng)前的執(zhí)行結(jié)果進(jìn)行封裝返回固定的數(shù)據(jù)格式等等。

執(zhí)行該示例后,訪問 http://127.0.0.1:8199/api.v2/user/list?token=123456 查看效果。


示例5,自定義日志處理

我們來更進(jìn)一步完善一下以上示例,我們將請(qǐng)求日志包括狀態(tài)碼輸出到終端。這里我們必須得使用”全局中間件”了,這樣可以攔截處理到所有的服務(wù)請(qǐng)求,甚至?404?請(qǐng)求。

package main

import (
	"net/http"

	"github.com/gogf/gf/v2/frame/g"
	"github.com/gogf/gf/v2/net/ghttp"
)

func MiddlewareAuth(r *ghttp.Request) {
	token := r.Get("token")
	if token.String() == "123456" {
		r.Middleware.Next()
	} else {
		r.Response.WriteStatus(http.StatusForbidden)
	}
}

func MiddlewareCORS(r *ghttp.Request) {
	r.Response.CORSDefault()
	r.Middleware.Next()
}

func MiddlewareLog(r *ghttp.Request) {
	r.Middleware.Next()
	errStr := ""
	if err := r.GetError(); err != nil {
		errStr = err.Error()
	}
	g.Log().Println(r.Response.Status, r.URL.Path, errStr)
}

func main() {
	s := g.Server()
	s.SetConfigWithMap(g.Map{
		"AccessLogEnabled": false,
		"ErrorLogEnabled":  false,
	})
	s.Use(MiddlewareLog, MiddlewareCORS)
	s.Group("/api.v2", func(group *ghttp.RouterGroup) {
		group.Middleware(MiddlewareAuth)
		group.ALL("/user/list", func(r *ghttp.Request) {
			panic("啊!我出錯(cuò)了!")
		})
	})
	s.SetPort(8199)
	s.Run()
}



可以看到,我們注冊(cè)了一個(gè)全局的日志處理中間件以及跨域中間件,而鑒權(quán)中間件是注冊(cè)到?/api.v2?路由下。

執(zhí)行后,我們可以通過請(qǐng)求 http://127.0.0.1:8199/api.v2/user/list 和 http://127.0.0.1:8199/api.v2/user/list?token=123456 對(duì)比來查看效果,并查看終端的日志輸出情況。


以上內(nèi)容是否對(duì)您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)