# 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("过滤校验")
}
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)
2
3
4
5
6
7
注意观察这个定义,我们认为
filter只是一种特殊的handler,所以在这里FilterFunc是HandleFunc的别名。从这个角度来说,我们认为最后处理请求的地方,就是最后的一个filter。
现在我们来看看InsertFilter的定义:
// InserFilter see HttpServer.InsertFilter
func InsertFilter(pattern string, pos int, filter FilterFunc, opts ...FilterOpt) *HttpServer {
// ...
}
2
3
4
各个参数的含义是:
pattern等价于注册路由时候的pattern,也可以理解为匹配规则;pos表示位置,准确来说,是指请求执行的各个阶段;filter则是逻辑代码;opts是filter的一些选项;
比较难理解的是 pos,它有很多个取值:
BeforeStatic静态地址之前BeforeRouter寻找路由之前,从这里开始,我们就能够获得session了BeforeExec找到路由之后,开始执行相应的 Controller 之前AfterExec执行完 Controller 逻辑之后执行的过滤器FinishRouter执行完逻辑之后执行的过滤器
而 opts 对应三个选项:
web.WithReturnOnOutput: 设置returnOnOutput的值(默认true), 如果在进行到此过滤之前已经有输出,是否不再继续执行此过滤器,默认设置为如果前面已有输出(参数为true),则不再执行此过滤器;web.WithResetParams: 是否重置filter的参数,默认是false,因为在filter的pattern和本身的路由的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)
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)
2
3
4
5
6
7
8
9
10
11
12
13
# Filter 和 Filter Chain
前面提到的filter有一个固然的缺陷,就是它们是单向的。
例如,在考虑接入Opentracing和prometheus的时候,我们就遇到了这种问题。
考虑到这是一个通用的场景,我们在已有 Filter 的基础上,支持了Filter-Chain设计模式。
type FilterChain func(next FilterFunc) FilterFunc
例如一个非常简单的例子:
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
}
})
}
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"))
}
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"))
}
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"))
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))
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
beego内部读取这个token并且进行解码,得到携带的用户名和密码。beego会比较用户名和密码是否匹配,这个过程需要开发者在初始化过滤器的时候告诉beego如何匹配用户名和密码。
初始化这个过滤器有两种方法,最基础的做法是:
// import "github.com/beego/beego/v2/server/web/filter/auth"
web.InsertFilter("/", web.BeforeRouter, auth.Basic("your username", "your pwd"))
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()
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")))
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,
}))
2
3
4
在这种设置之下,不管什么域名之下过来的请求,都是被允许的。如果想做精细化控制,可以调整Options的参数值。
# Rate Limit Filter
限流过滤器,使用的是令牌桶的实现。接入方式是:
// import "github.com/beego/beego/v2/server/web/filter/ratelimit"
web.InsertFilter("/", web.BeforeRouter, ratelimit.NewLimiter())
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))
2
3
4
核心就是通过参数来控制使用什么类型的session。
具体的细节可以参考session