# Web 过滤器

filter是我们 Beego 提供的 AOP 的解决方案。不仅仅是在web中应用,也是在其余模块中应用。

在 Beego 中,filter 承担两方面的职责,一方面是作为AOP的实现,一方面是作为请求生命周期的钩子。所以要理解filter要先理解 Beego 的请求处理过程。

# 简单例子

我们来看一个最简单的例子:

import (
	"fmt"

	"github.com/beego/beego/v2/server/web"
	"github.com/beego/beego/v2/server/web/context"
)

func main() {

	ctrl := &MainController{}
	// 注册路由过滤器
	web.InsertFilter("/user/*", web.BeforeExec, filterFunc)
	web.Run()
}

// 定义 filter
func filterFunc(ctx *context.Context) {
	// 只是输出一个语句
	fmt.Println("过滤校验")
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这里我们可以看到,所谓的filter,就是一个参数是*context.Context的方法,这个是它的定义:

// FilterFunc defines a filter function which is invoked before the controller handler is executed.
// It's a alias of HandleFunc
// In fact, the HandleFunc is the last Filter. This is the truth
type FilterFunc = HandleFunc

// HandleFunc define how to process the request
type HandleFunc func(ctx *beecontext.Context)
1
2
3
4
5
6
7

注意观察这个定义,我们认为filter只是一种特殊的handler,所以在这里FilterFuncHandleFunc的别名。从这个角度来说,我们认为最后处理请求的地方,就是最后的一个filter

现在我们来看看InsertFilter的定义:

// InserFilter see HttpServer.InsertFilter
func InsertFilter(pattern string, pos int, filter FilterFunc, opts ...FilterOpt) *HttpServer {
	// ...
}
1
2
3
4

各个参数的含义是:

  1. pattern等价于注册路由时候的pattern,也可以理解为匹配规则;
  2. pos 表示位置,准确来说,是指请求执行的各个阶段;
  3. filter 则是逻辑代码;
  4. optsfilter 的一些选项;

比较难理解的是 pos,它有很多个取值:

  • BeforeStatic 静态地址之前
  • BeforeRouter 寻找路由之前,从这里开始,我们就能够获得session
  • BeforeExec 找到路由之后,开始执行相应的 Controller 之前
  • AfterExec 执行完 Controller 逻辑之后执行的过滤器
  • FinishRouter 执行完逻辑之后执行的过滤器

opts 对应三个选项:

  • web.WithReturnOnOutput: 设置 returnOnOutput 的值(默认true), 如果在进行到此过滤之前已经有输出,是否不再继续执行此过滤器,默认设置为如果前面已有输出(参数为true),则不再执行此过滤器;
  • web.WithResetParams: 是否重置filter的参数,默认是false,因为在filterpattern和本身的路由的pattern冲突的时候,可以把filter的参数重置,这样可以保证在后续的逻辑中获取到正确的参数,例如设置了/api/* 的 filter,同时又设置了 /api/docs/* 的 router,那么在访问 /api/docs/swagger/abc.js 的时候,在执行filter的时候设置 :splat 参数为 docs/swagger/abc.js,但是如果该选项为 false,就会在执行路由逻辑的时候保持 docs/swagger/abc.js,如果设置了true,就会重置 :splat 参数;
  • web.WithCaseSensitive: 是否大小写敏感;

如果不清楚如何使用这些选项,最好的方法是自己写几个测试来试验一下它们的效果。

我们在看一个验证登录态的例子。该例子是假设启用了 Beego 的session模块:

var FilterUser = func(ctx *context.Context) {
    _, ok := ctx.Input.Session("uid").(int)
    if !ok && ctx.Request.RequestURI != "/login" {
        ctx.Redirect(302, "/login")
    }
}

web.InsertFilter("/*", web.BeforeRouter, FilterUser)
1
2
3
4
5
6
7
8

要注意,要访问Session方法,pos参数不能设置为BeforeStatic

pattern 的设置,可以参考路由规则

# 过滤器修改原始路由

有些时候,我们可能想篡改已经某些已经注册的路由。例如原本我们的

如下示例实现了如何实现自己的路由规则:

var UrlManager = func(ctx *context.Context) {
    // 数据库读取全部的 url mapping 数据
	urlMapping := model.GetUrlMapping()
	for baseurl,rule:=range urlMapping {
		if baseurl == ctx.Request.RequestURI {
			ctx.Input.RunController = rule.controller
			ctx.Input.RunMethod = rule.method
			break
		}
	}
}

web.InsertFilter("/*", web.BeforeRouter, web.UrlManager)
1
2
3
4
5
6
7
8
9
10
11
12
13

# Filter 和 Filter Chain

前面提到的filter有一个固然的缺陷,就是它们是单向的。

例如,在考虑接入Opentracingprometheus的时候,我们就遇到了这种问题。

考虑到这是一个通用的场景,我们在已有 Filter 的基础上,支持了Filter-Chain设计模式。

type FilterChain func(next FilterFunc) FilterFunc
1

例如一个非常简单的例子:

package main

import (
	"github.com/beego/beego/v2/core/logs"
	"github.com/beego/beego/v2/server/web"
	"github.com/beego/beego/v2/server/web/context"
)

func main() {
	web.InsertFilterChain("/*", func(next web.FilterFunc) web.FilterFunc {
		return func(ctx *context.Context) {
			// do something
			logs.Info("hello")
			// don't forget this
			next(ctx)

			// do something
		}
	})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

这个例子里面,我们只是输出了一句"hello",就调用了下一个 Filter。

在执行完next(ctx)之后,实际上,如果后面的 Filter 没有中断整个流程,那么这时候OutPut对象已经被赋值了,意味着能够拿到响应码等数据。

# 内置 Filter

我们提供了一系列的 filter,你可以看情况,决定是否启用。

# Prometheus 例子

package main

import (
	"time"

	"github.com/beego/beego/v2/server/web"
	"github.com/beego/beego/v2/server/web/filter/prometheus"
)

func main() {
	// we start admin service
	// Prometheus will fetch metrics data from admin service's port
	web.BConfig.Listen.EnableAdmin = true

	web.BConfig.AppName = "my app"

	ctrl := &MainController{}
	web.Router("/hello", ctrl, "get:Hello")
	fb := &prometheus.FilterChainBuilder{}
	web.InsertFilterChain("/*", fb.FilterChain)
	web.Run(":8080")
	// after you start the server
	// and GET http://localhost:8080/hello
	// access http://localhost:8088/metrics
	// you can see something looks like:
	// http_request_web_sum{appname="my app",duration="1002",env="prod",method="GET",pattern="/hello",server="webServer:1.12.1",status="200"} 1002
	// http_request_web_count{appname="my app",duration="1002",env="prod",method="GET",pattern="/hello",server="webServer:1.12.1",status="200"} 1
	// http_request_web_sum{appname="my app",duration="1004",env="prod",method="GET",pattern="/hello",server="webServer:1.12.1",status="200"} 1004
	// http_request_web_count{appname="my app",duration="1004",env="prod",method="GET",pattern="/hello",server="webServer:1.12.1",status="200"} 1
}

type MainController struct {
	web.Controller
}

func (ctrl *MainController) Hello() {
	time.Sleep(time.Second)
	ctrl.Ctx.ResponseWriter.Write([]byte("Hello, world"))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

别忘记了开启prometheus的端口。在你没有启动admin服务的时候,需要自己手动开启。

# Opentracing 例子

package main

import (
	"time"

	"github.com/beego/beego/v2/server/web"
	"github.com/beego/beego/v2/server/web/filter/opentracing"
)

func main() {
	// don't forget this to inject the opentracing API's implementation
	// opentracing2.SetGlobalTracer()

	web.BConfig.AppName = "my app"

	ctrl := &MainController{}
	web.Router("/hello", ctrl, "get:Hello")
	fb := &opentracing.FilterChainBuilder{}
	web.InsertFilterChain("/*", fb.FilterChain)
	web.Run(":8080")
	// after you start the server
}

type MainController struct {
	web.Controller
}

func (ctrl *MainController) Hello() {
	time.Sleep(time.Second)
	ctrl.Ctx.ResponseWriter.Write([]byte("Hello, world"))
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

别忘了调用opentracing库的SetGlobalTracer方法,注入真正的opentracing API的实现。

# Api Auth Filter

鉴权过滤器用起来要理解两个点:

  • 如何接入这个过滤器
  • 如何生成正确的签名

接入鉴权过滤器有两种做法,最基本的做法是:


// import "github.com/beego/beego/v2/server/web/filter/apiauth"
web.InsertFilter("/", web.BeforeRouter, apiauth.APIBasicAuth("myid", "mykey"))
1
2
3

其中mykey是用于校验签名的密钥,也是上游发起调用的时候需要用到的密钥。这种接入方案非常简单,beego内部实现会从请求参数里面读出appid,而后如果appid恰好是myid,则会用mykey来生成签名,和同样从参数里面读出来的签名进行比较。如果两者相等,则会处理请求,否则会拒绝请求,返回403错误。

另外一种用法是自定义根据appid来查找密钥的方法。接入方式是:

    // import "github.com/beego/beego/v2/server/web/filter/apiauth"
	web.InsertFilter("/", web.BeforeRouter, apiauth.APISecretAuth(func(appid string) string {
		// 这里是你定义的如何根据 app id 来查找密钥的方法
		// 比如说这种简单的做法,生产勿用
		return appid + "key"
	}, 300))
1
2
3
4
5
6

注意,300代表的是超时时间。

使用这个过滤器,要注意以下几点:

  • 过滤器依赖于从请求参数中读取appid,并且根据appid来查找密钥
  • 过滤器依赖于从请求参数中读取timestamp,即时间戳,它的时间格式是2006-01-02 15:04:05
  • 过滤器依赖于从请求参数中读取签名signature,并且beego会用读取到的签名和自己根据密钥生成的签名进行比较,也就是鉴权

此外,作为调用方,可以直接使用apiauth.Signature方法来生成签名,放到请求参数里面去请求下游接口。

注意,我们不建议在公共API上使用这个鉴权过滤器。因为该实现只具备基础的功能,并不具备很强的安全性——它极度依赖于密钥。如果自身的密钥暴露出去之后,那么攻击者可以轻易根据beego使用的加密方式,生成正确的密钥。具体的更高安全性的鉴权实现,已经脱离了beego的范畴,有需要的开发可以自行了解。

# Auth Filter

这个过滤器和前面的鉴权过滤器十分相像。但是两者的机制不同。apiauth使用的是签名机制,侧重于应用之间互相调用。而这个应该叫做认证过滤器,侧重的是身份识别,其内部机制是使用用户名和密码,类似于登录过程。

该过滤器,会从请求头部Authorization里面读取token。目前来说,beego只支持Basic这一种加密方式。即请求的头部应该包含:

Authorization Basic your-token
1

beego内部读取这个token并且进行解码,得到携带的用户名和密码。beego会比较用户名和密码是否匹配,这个过程需要开发者在初始化过滤器的时候告诉beego如何匹配用户名和密码。

初始化这个过滤器有两种方法,最基础的做法是:

// import "github.com/beego/beego/v2/server/web/filter/auth"
web.InsertFilter("/", web.BeforeRouter, auth.Basic("your username", "your pwd"))
1
2

那么beego会用Basic方法传入的账号密码和从token里面解析出来的值做比较,账号和密码同时相等的时候,请求才会被处理。

也可以指定账号密码的匹配方式:

	// import "github.com/beego/beego/v2/server/web/filter/auth"
	web.InsertFilter("/", web.BeforeRouter, auth.NewBasicAuthenticator(func(username, pwd string) bool {
		// 这里是你的校验逻辑。username, pwd 则是从请求头部解密出来的
	}, "your-realm"))
	web.Run()
1
2
3
4
5

其中your-realm只是在校验失败的时候作为一个错误信息放到响应头部。

# Authz Filter

这个过滤器同样是鉴权,而不是认证。它和前面两个过滤器比起来,它侧重的是用户是否具有访问某个资源的权限。它和Auth Filter一样,从Authorization的头部里面解析用户名,所不同的是,这个过滤器并不会理会密码。

或者说,它应该叫做Casbin过滤器。具体的可以阅读Casbin github (opens new window)。注意,beego依旧使用的是它的v1版本,而目前来看,它们已经升级到了v2版本。

之后,该过滤器会结合http method和请求路径,判断该用户是否权限。如果有权限,那么beego就会处理请求。

使用该过滤器的方式是:

// import "github.com/beego/beego/v2/server/web/filter/authz"
web.InsertFilter("/", web.BeforeRouter, authz.NewAuthorizer(casbin.NewEnforcer("path/to/basic_model.conf", "path/to/basic_policy.csv")))

1
2
3

关于更多的Casbin的信息,请参考Casbin github (opens new window)

# CORS Filter

解决跨域问题的过滤器。使用该过滤器非常简单:

	// import "github.com/beego/beego/v2/server/web/filter/cors"
	web.InsertFilter("/", web.BeforeRouter, cors.Allow(&cors.Options{
		AllowAllOrigins: true,
	}))
1
2
3
4

在这种设置之下,不管什么域名之下过来的请求,都是被允许的。如果想做精细化控制,可以调整Options的参数值。

# Rate Limit Filter

限流过滤器,使用的是令牌桶的实现。接入方式是:


// import "github.com/beego/beego/v2/server/web/filter/ratelimit"
web.InsertFilter("/", web.BeforeRouter, ratelimit.NewLimiter())

1
2
3
4

令牌桶算法主要受到两个参数的影响,一个是容量,一个是速率。默认情况下,容量被设置为100,而速率被设置为每十毫秒产生一个令牌。

有很多选项可以控制这个过滤器的行为:

  • WithCapacity:控制容量
  • WithRate:速率控制
  • WithRejectionResponse:拒绝请求的响应
  • WithSessionKey:限流对象。例如如果相对某一个API限流,则可以返回该API的路由。在这种情况下,那么不能使用web.BeforeRouter,而应该使用web.BeforeExec

# Session Filter

这是一个试验性质,我们尝试支持在不同维度上控制session。所以引入了这个filter

	// "github.com/beego/beego/v2/server/web"
	// "github.com/beego/beego/v2/server/web/filter/session"
	// websession "github.com/beego/beego/v2/server/web/session"
	web.InsertFilterChain("/need/session/path", session.Session(websession.ProviderMemory))
1
2
3
4

核心就是通过参数来控制使用什么类型的session

具体的细节可以参考session

# 相关文档