Traefik Middlewares 是一个处于路由和后端服务之前的中间件,在外部流量进入 Traefik,且路由规则匹配成功后,将流量发送到对应的后端服务前,先将其发给中间件进行一些列处理(类似于过滤器链 Filter,进行一系列处理),例如,添加 Header 头信息、鉴权、流量转发、处理访问路径前缀、IP 白名单等等,经过一个或者多个中间件处理完成后,再发送给后端服务。
在目前的版本中,Traefik 官方已经内置了各种不同功能类型的中间件,其中有的可以修改请求,头信息,有的负责重定向以及其他可添加身份验证等等,而且中间件还可以通过链式组合的方式来适应各种情况。因此,自从 Traefik 2.X 版本发布以来受到了很大的关注,特别是提供的中间件机制非常深受广大技术人员的欢迎,但是目前对于用户来说能使用的也只有官方提供的中间件,这对于某些特殊场景可能就无法满足。更何况官方目前没有提供自定义 Middleware 加入到 Traefik 的解决方案,惟有对官方的源代码进行适应性改造的方式。当然,我们也可以建议官方提供自定义插件方式,类似与 coreDNS 方式,通过外挂方式以实现我们所需要的功能。于是,我们将目标聚焦在自定义中间件上,因为目前官方没有提供合理的解决方案将我们所定义的中间件集成到 Traefik 中,所以只能直接对 Traefik 组件基于实际的业务需求进行 2 次开发,下面我们以一个简单的示例来说明下如何自定义一个 Traefik 中间件。
环境版本:traefik-v2.2.8
场景描述:
以添加“验证 Token” 功能为例,简要解析其插件使用方法。此插件主要功能:获取请求在 Header 中添加的 Token,后端请求服务校验 Token 是否正确,若正确,则继续请求后端;反之,则直接返回错误信息。
针对自定义插件的功能实现,主要涉及以下代码修改或调整:
1、在 pkg/middleware/auth 文件夹中自定义插件主逻辑文件
在本案例中,我们新建一个名为 “token_auth” 的 GO 文件,其作为主逻辑文件以声明所封装的自定义功能插件,其源码如下所示:
package auth
import (
"context"
"encoding/json"
"fmt"
"github.com/containous/traefik/v2/pkg/config/dynamic"
"github.com/containous/traefik/v2/pkg/log"
"github.com/containous/traefik/v2/pkg/middlewares"
"github.com/containous/traefik/v2/pkg/tracing"
"github.com/opentracing/opentracing-go/ext"
"io/ioutil"
"net/http"
"net/url"
"strings"
"time"
)
const (
tokenTypeName = "TokenAuthType"
)
type tokenAuth struct {
address string
next http.Handler
name string
client http.Client
}
type commonResponse struct {
Status int32 `json:"status"`
Message string `json:"message"`
}
// NewToken creates a passport auth middleware.
func NewToken(ctx context.Context, next http.Handler, config dynamic.TokenAuth, name string) (http.Handler, error) {
log.FromContext(middlewares.GetLoggerCtx(ctx, name, tokenTypeName)).Debug("Creating middleware")
// 插件结构体
ta := &tokenAuth{
address: config.Address,
next: next,
name: name,
}
// 创建请求其他服务的 http client
ta.client = http.Client{
CheckRedirect: func(r *http.Request, via []*http.Request) error {
return http.ErrUseLastResponse
},
Timeout: 30 * time.Second,
}
return ta, nil
}
func (ta *tokenAuth) GetTracingInformation() (string, ext.SpanKindEnum) {
return ta.name, ext.SpanKindRPCClientEnum
}
func (ta tokenAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
logger := log.FromContext(middlewares.GetLoggerCtx(req.Context(), ta.name, tokenTypeName))
errorMsg := []byte("{\"code\":10000,\"message\":\"token校验失败!\"}")
// 从 header 中获取 token
token := req.Header.Get("token")
if token == "" {
logMessage := fmt.Sprintf("Error calling %s. Cause token is empty", ta.address)
traceAndResponseDebug(logger, rw, req, logMessage, []byte("{\"statue\":10000,\"message\":\"token is empty\"}"), http.StatusBadRequest)
return
}
// 以下都是请求其他服务验证 token
// 构建请求体
form := url.Values{}
form.Add("token", token)
passportReq, err := http.NewRequest(http.MethodPost, ta.address, strings.NewReader(form.Encode()))
tracing.LogRequest(tracing.GetSpan(req), passportReq)
if err != nil {
logMessage := fmt.Sprintf("Error calling %s. Cause %s", ta.address, err)
traceAndResponseDebug(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest)
return
}
tracing.InjectRequestHeaders(req)
passportReq.Header.Set("Content-Type", "application/x-www-form-urlencoded")
// post 请求
passportResponse, forwardErr := ta.client.Do(passportReq)
if forwardErr != nil {
logMessage := fmt.Sprintf("Error calling %s. Cause: %s", ta.address, forwardErr)
traceAndResponseError(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest)
return
}
logger.Info(fmt.Sprintf("Passport auth calling %s. Response: %+v", ta.address, passportResponse))
// 读 body
body, readError := ioutil.ReadAll(passportResponse.Body)
if readError != nil {
logMessage := fmt.Sprintf("Error reading body %s. Cause: %s", ta.address, readError)
traceAndResponseError(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest)
return
}
defer passportResponse.Body.Close()
if passportResponse.StatusCode != http.StatusOK {
logMessage := fmt.Sprintf("Remote error %s. StatusCode: %d", ta.address, passportResponse.StatusCode)
traceAndResponseDebug(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest)
return
}
// 解析 body
var commonRes commonResponse
err = json.Unmarshal(body, &commonRes)
if err != nil {
logMessage := fmt.Sprintf("Body unmarshal error. Body: %s", body)
traceAndResponseError(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest)
return
}
// 判断返回值,非0代表验证失败
if commonRes.Status != 0 {
logMessage := fmt.Sprintf("Body status is not success. Status: %d", commonRes.Status)
traceAndResponseDebug(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest)
return
}
ta.next.ServeHTTP(rw, req)
}
func traceAndResponseDebug(logger log.Logger, rw http.ResponseWriter, req *http.Request, logMessage string, errorMsg []byte, status int) {
logger.Debug(logMessage)
tracing.SetErrorWithEvent(req, logMessage)
rw.Header().Set("Content-Type", "application/json;charset=UTF-8")
rw.WriteHeader(status)
_, _ = rw.Write(errorMsg)
}
func traceAndResponseInfo(logger log.Logger, rw http.ResponseWriter, req *http.Request, logMessage string, errorMsg []byte, status int) {
logger.Info(logMessage)
tracing.SetErrorWithEvent(req, logMessage)
rw.Header().Set("Content-Type", "application/json;charset=UTF-8")
rw.WriteHeader(status)
_, _ = rw.Write(errorMsg)
}
func traceAndResponseError(logger log.Logger, rw http.ResponseWriter, req *http.Request, logMessage string, errorMsg []byte, status int) {
logger.Debug(logMessage)
tracing.SetErrorWithEvent(req, logMessage)
rw.Header().Set("Content-Type", "application/json;charset=UTF-8")
rw.WriteHeader(status)
_, _ = rw.Write(errorMsg)
}
此时,其目录结构如下所示:
2、在 pkg/config/dynamic/middleware.go 添加动态配置映射
上述将我们自定义的 token_auth 中间件代码添加到了 Traefik 源码中,但是这远远不够,仅仅只是声明了中间件而已,此时,还需要将该中间件配置到 Traefik 的中间件中去才能生效。故此,需要在 pkg/config/dynamic/middleware.go 文件在 Middleware 结构体下面添加自定义脚本字段,以实现实体与配置文件之间的映射关系,具体源码如下所示:
// pkg/server/middleware/middlewares.go
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) {
/* ... */
// TokenAuth
if config.TokenAuth != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return auth.NewToken(ctx, next, *config.TokenAuth, middlewareName)
}
}
/* ... */
}
3、在 pkg/server/middleware/middlewares.go 构造插件
在动态配置中完成后,然后需要在服务端构建器中注册上面定义的 token_auth 中间件,代码位于 pkg/server/middleware/middlewares.go,在 buildConstructor 方法中添加上自定义中间件的信息,具体如下:
// pkg/server/middleware/middlewares.go
func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) {
/* ... */
// TokenAuth
if config.TokenAuth != nil {
if middleware != nil {
return nil, badConf
}
middleware = func(next http.Handler) (http.Handler, error) {
return auth.NewToken(ctx, next, *config.TokenAuth, middlewareName)
}
}
/* ... */
}
至此,我们已基本完成 token_auth 自定义中间件的开发工作。
接下来,我们就剩下重新编译、打包以及相关配置活动创建,具体如以下步骤所示:
1、重新编译打包 Traefik ,此处有多种方式可完成,以下为以 Go 工具进行操作的简要示例:
[administrator@JavaLangOutOfMemory ~] % go generate
[administrator@JavaLangOutOfMemory ~] % export GOPROXY=https://goproxy.cn
[administrator@JavaLangOutOfMemory ~] % export GO111MODULE=on
[administrator@JavaLangOutOfMemory ~] % go build -v -o traefik ./cmd/traefik
2、创建相关配置文件
以 traefik.yaml 为例,需要创建基于 middlewares 和 routers 的配置文件,具体如下所示:
插件配置
http:
middlewares:
# token验证
token-auth:
tokenAuth:
address: <http://demo.example.com/token_info>
动态路由配置
http:
routers:
svc:
entryPoints:
- web
middlewares:
- token-auth
service: svc
rule: PathPrefix(`/list`)
此时,新添加的 Token_auth 功能插件就可以发挥其作用,接下来,我们再对其进行重启操作以使其生效,其相关命令行如下:
[administrator@JavaLangOutOfMemory ~] %./traefik --configfile=traefik.yaml
至此,基于 Traefik 的一个简单的自定义插件开发工作到此为止,然后结合实际的业务逻辑进行测试验证即可。
参考资料