# Web Filter
filter is the solution for AOP that we provide at Beego. It is applied not only in web, but also in the rest of the modules.
In Beego, filter has two responsibilities, one is as an implementation of AOP and the other is as a hook in the request lifecycle. So to understand filter you have to understand Beego's request handling process first.
# Quick Start
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()
}
func filterFunc(ctx *context.Context) {
fmt.Println("do something here")
}
Here we can see that the so-called filter, is a method whose parameter is *context.Context, and this is its definition:
// 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)
Note that we consider
filterto be just a special kind ofhandler, so hereFilterFuncis an alias forHandleFunc. From this perspective, we think that the last place to process a request is the lastfilter.
And InsertFilter:
// InserFilter see HttpServer.InsertFilter
func InsertFilter(pattern string, pos int, filter FilterFunc, opts ...FilterOpt) *HttpServer {
// ...
}
pattern: string or regex to match against router rules. Use/*to match all.pos: the place to execute the Filter. There are five fixed parameters representing different execution processes.- web.BeforeStatic: Before finding the static file.
- web.BeforeRouter: Before finding router.
- web.BeforeExec: After finding router and before executing the matched Controller.
- web.AfterExec: After executing Controller.
- web.FinishRouter: After finishing router.
filter: filter function type FilterFunc func(*context.Context)opts:web.WithReturnOnOutput: sets the value ofreturnOnOutput(defaulttrue), if there is already output before this filter is performed, whether to not continue to execute this filter, the default setting is that if there is already output before (parametertrue), then this filter will not be executed.web.WithResetParams: Whether to reset thefilterparameter, the default isfalse, because in case of conflict between thepatternof thefilterand thepatternof the route itself, you can reset thefilterparameter, so as to ensure that the correct parameter is obtained in the subsequent logic, for example, if the filter of/api/*is set, and the router of/api/docs/*is set at the same time, then when the router of/api/docs/*is executed, then the correct parameter is obtained in the subsequent logic. For example, if you set the filter of/api/*and also set the router of/api/docs/*, then when you access/api/docs/swagger/abc.js, set:splatparameter todocs/swagger/abc.jswhen executingfilter, but if the option isfalse, it will keepdocs/swagger/abc.jswhen executing the routing logic, and reset the:splatparameter iftrueis set.web.WithCaseSensitive: case sensitive;
If it is not clear how to use these options, the best way is to write a few tests yourself to experiment with their effects.
Here is an example to authenticate if the user is logged in for all requests:
var FilterUser = func(ctx *context.Context) {
if strings.HasPrefix(ctx.Input.URL(), "/login") {
return
}
_, ok := ctx.Input.Session("uid").(int)
if !ok {
ctx.Redirect(302, "/login")
}
}
web.InsertFilter("/*", web.BeforeRouter, FilterUser)
Be aware that to access the Session method, the pos parameter cannot be set to BeforeStatic.
More details for pattern refer router
# Update Existing Router
Custom router during runtime:
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)
# Filter And FilterChain
In v1.x, we can't invoke next Filter inside a Filter. So we got a problem: we could not do something "surrounding" request execution.
For example, if we want to do:
func filter() {
// do something before serving request
handleRequest()
// do something after serving request
}
The typical cases are tracing and metrics.
So we enhance Filter by designing a new interface:
type FilterChain func(next FilterFunc) FilterFunc
Here is a simple example:
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
}
})
}
In this example, we only output "hello" and then we invoke next 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"))
}
If you don't use Beego's admin service, don't forget to expose prometheus's port.
# 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"))
}
Don't forget to using SetGlobalTracer to initialize opentracing.
# Builtin Filters
We provide a series of filters that you can enable or disable, depending on the situation.
# 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"))
}
# 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"))
}
# Api Auth Filter
There are two points to understand when using the api auth filter:
- How to access this filter
- How to generate the correct signature
Basic usage:
// import "github.com/beego/beego/v2/server/web/filter/apiauth"
web.InsertFilter("/", web.BeforeRouter, apiauth.APIBasicAuth("myid", "mykey"))
Where mykey is the key used to verify the signature, and is also the key needed when the upstream call is initiated. This access scheme is very simple, beego internal implementation will read appid from the request, and then if appid happens to be myid, mykey will be used to generate the signature and compare it with the signature also read from the parameters. If they are equal, the request will be processed, otherwise the request will be rejected and a 403 error will be returned.
Another usage is a custom method of finding keys based on appid:
// import "github.com/beego/beego/v2/server/web/filter/apiauth"
web.InsertFilter("/", web.BeforeRouter, apiauth.APISecretAuth(func(appid string) string {
// Here's how you define how to find the key based on the app id
return appid + "key"
}, 300))
Note that 300 represents the timeout parameter.
- The filter relies on reading
appidfrom the request parameters and finding the key based onappid - The filter relies on reading
timestamp, the timestamp, from the request parameter, which is in the format2006-01-02 15:04:05 - The filter relies on reading the signature
signaturefrom the request parameters, andbeegocompares the read signature with its own signature generated from the key, i.e. forensics
In addition, as the caller, you can use the apiauth.Signature method directly to generate a signature to put inside the request to call the downstream.
Note that we do not recommend using this forensic filter on the public API. This is because the implementation has only basic functionality and does not have strong security - it is extremely key dependent. If its own key is exposed, then an attacker can easily generate the correct key based on the encryption method used by beego. The specific higher-security forensic implementation is out of the scope of beego and can be understood by developers who need it.
# Auth Filter
This filter is very similar to the previous api auth filter. But the two have different mechanisms. apiauth uses a signature mechanism and focuses on applications calling each other. This one should be called an authentication filter, which focuses on identification, and its internal mechanism is to use a username and password, similar to the login process.
This filter, reads the token from the request header Authorization. Currently, beego only supports the Basic encryption method, looks like:
Authorization Basic your-token
beego internally reads this token and decodes it to get the username and password it carries. beego compares the username and password to see if they match, a process that requires the developer to tell beego how to match the username and password when initializing the filter.
The basic usage:
// import "github.com/beego/beego/v2/server/web/filter/auth"
web.InsertFilter("/", web.BeforeRouter, auth.Basic("your username", "your pwd"))
Or:
// import "github.com/beego/beego/v2/server/web/filter/auth"
web.InsertFilter("/", web.BeforeRouter, auth.NewBasicAuthenticator(func(username, pwd string) bool {
// validation here
}, "your-realm"))
web.Run()
where your-realm is simply placed in the response header as an error message if the validation fails。
# Authz Filter
This filter is also authentication, not authentication. It focuses on whether the user has access to a resource compared to the previous two filters. Like the Auth Filter, it parses the username from the Authorization header, except that this filter doesn't care about passwords.
Or rather, it should be called Casbin filter. For details, you can read Casbin github (opens new window). Note that beego is still using its v1 version, while as of now they have been upgraded to v2.
After that, the filter combines the http method and the request path to determine if the user has permission. If there is permission, then beego will process the request.
Example:
// 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")))
More details refer to 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,
}))
With this setting, requests coming under whatever domain are allowed. If you want to do fine-grained control, you can adjust the parameter value of Options。
# Rate Limit Filter
// import "github.com/beego/beego/v2/server/web/filter/ratelimit"
web.InsertFilter("/", web.BeforeRouter, ratelimit.NewLimiter())
The token bucket algorithm is mainly influenced by two parameters, one is the capacity and the other is the rate. By default, the capacity is set to 100 and the rate is set to generate one token every ten milliseconds.
WithCapacityWithRateWithRejectionResponse: the response if rejecting the requestWithSessionKey: For example if the flow is restricted relative to a particularAPI, then the route for thatAPIcan be returned. In this case, thenweb.BeforeRoutershould not be used, butweb.BeforeExecshould be used instead.
# Session Filter
This is experimental in nature, we try to support controlling session in different dimensions. So this filter is introduced.
// "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))
More details refer session