Traefik Middleware 插件实践

简介: 话说,作为一款开源的边缘路由器,Traefik 或多或少算是一款比较牛逼的新生代组件,其不仅可以作为接入层组件,实现流量转发等相关功能,同时也可以支撑网关层的相关功能,尤其是 2.x 版本中的自定义中间件功能。

    话说,作为一款开源的边缘路由器,Traefik 或多或少算是一款比较牛逼的新生代组件,其不仅可以作为接入层组件,实现流量转发等相关功能,同时也可以支撑网关层的相关功能,尤其是 2.x 版本中的自定义中间件功能。

    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 的一个简单的自定义插件开发工作到此为止,然后结合实际的业务逻辑进行测试验证即可。


  # 参考资料

相关文章
|
4月前
|
Kubernetes 中间件 容器
Rancher 系列文章 -K3s Traefik MiddleWare 报错 -Failed to create middleware keys
Rancher 系列文章 -K3s Traefik MiddleWare 报错 -Failed to create middleware keys
|
中间件
基于traefik v2 写一个控制http请求方法的middleware配置
基于Traefik v2编写一个控制HTTP请求方法的中间件
432 0
|
4月前
|
消息中间件 存储 负载均衡
消息中间件的选择:RabbitMQ是一个明智的选择
消息中间件的选择:RabbitMQ是一个明智的选择
85 0
|
3月前
|
消息中间件 存储 中间件
【消息中间件】详解三大MQ:RabbitMQ、RocketMQ、Kafka
【消息中间件】详解三大MQ:RabbitMQ、RocketMQ、Kafka
300 0
|
2月前
|
消息中间件 编解码 Docker
Docker部署RabbitMQ消息中间件
【7月更文挑战第4天】Docker部署RabbitMQ消息中间件
229 3
|
29天前
|
消息中间件 Java 测试技术
消息中间件RabbitMQ---SpringBoot整合RabbitMQ【三】
这篇文章是关于如何在SpringBoot应用中整合RabbitMQ的消息中间件。内容包括了在SpringBoot项目中添加RabbitMQ的依赖、配置文件设置、启动类注解,以及如何通过单元测试来创建交换器、队列、绑定,并发送和接收消息。文章还介绍了如何配置消息转换器以支持对象的序列化和反序列化,以及如何使用注解`@RabbitListener`来接收消息。
消息中间件RabbitMQ---SpringBoot整合RabbitMQ【三】
|
29天前
|
消息中间件 Docker 容器
消息中间件RabbitMQ---Docker安装RabbitMQ、以及RabbitMQ的基本使用【二】
这篇文章提供了RabbitMQ的安装和基本使用教程,包括如何使用Docker拉取RabbitMQ镜像、创建容器、通过浏览器访问管理界面,以及如何创建交换机、队列、绑定和使用direct、fanout和topic三种类型的交换器进行消息发布和接收的测试。
消息中间件RabbitMQ---Docker安装RabbitMQ、以及RabbitMQ的基本使用【二】
|
29天前
|
消息中间件 存储 网络协议
消息中间件RabbitMQ---概述和概念 【一】
该文章提供了对消息中间件RabbitMQ的全面概述,包括其核心概念、工作原理以及与AMQP和JMS的关系。
消息中间件RabbitMQ---概述和概念 【一】
|
2月前
|
消息中间件 监控 负载均衡
中间件RabbitMQ性能瓶颈
【7月更文挑战第13天】
138 11
|
2月前
|
消息中间件 NoSQL Kafka
消息中间件(RocketMQ、RabbitMQ、ActiveMQ、Redis、kafka、ZeroMQ)以及之间的区别
消息中间件(RocketMQ、RabbitMQ、ActiveMQ、Redis、kafka、ZeroMQ)以及之间的区别