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


  # 参考资料

相关文章
|
2月前
|
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请求方法的中间件
382 0
|
8月前
|
NoSQL Java Redis
阿里Java高级岗中间件二面:GC+IO+JVM+多线程+Redis+数据库+源码
虽然“钱多、事少、离家近”的工作可能离技术人比较远,但是找到一份合适的工作,其实并不像想象中那么难。但是,有些技术人确实是认真努力工作,但在面试时表现出的能力水平却不足以通过面试,或拿到高薪,其实不外乎以下 2 个原因:
|
8月前
|
算法 NoSQL Java
2023年阿里高频Java面试题:分布式+中间件+高并发+算法+数据库
又到了一年一度的金九银十,互联网行业竞争是一年比一年严峻,作为工程师的我们唯有不停地学习,不断的提升自己才能保证自己的核心竞争力从而拿到更好的薪水,进入心仪的企业(阿里、字节、美团、腾讯.....)
|
8月前
|
算法 NoSQL Java
2021年阿里高频Java面试题:分布式+中间件+高并发+算法+数据库
又到了一年一度的金九银十,互联网行业竞争是一年比一年严峻,作为工程师的我们唯有不停地学习,不断的提升自己才能保证自己的核心竞争力从而拿到更好的薪水,进入心仪的企业(阿里、字节、美团、腾讯.....)
|
9月前
|
消息中间件 数据采集 Java
开发神技!阿里消息中间件进阶手册限时开源,请接住我的下巴
相信大家在实际工作中都用过消息中间件进行系统间数据交换,解决应用解耦、异步消息、流量削峰等问题,由此消息中间件的强大功能想必也不用我多说了!目前业界上关于消息中间件的实现多达好几十种,可谓百花齐放,所用的实现语言同样也五花八门。不管使用哪一个消息中间件,我们的目的都是实现高性能、高可用、可伸缩和最终一致性架构。
|
11月前
|
缓存 NoSQL 容灾
《Java应用提速(速度与激情)》——六、阿里中间件提速
《Java应用提速(速度与激情)》——六、阿里中间件提速
|
11月前
|
消息中间件 NoSQL Dubbo
阿里Java高级岗中间件二面:GC+IO+JVM+多线程+Redis+数据库+源码
一转眼,都2023年了,你是否在满意的公司?拿着理想的薪水? 虽然“钱多、事少、离家近”的工作可能离技术人比较远,但是找到一份合适的工作,其实并不像想象中那么难。但是,有些技术人确实是认真努力工作,但在面试时表现出的能力水平却不足以通过面试,或拿到高薪,其实不外乎以下 2 个原因: 第一,“知其然不知其所以然”。做了多年技术,开发了很多业务应用,但似乎并未思考过种种技术选择背后的逻辑。所以,他无法向面试官展现出自己未来技术能力的成长潜力。面试官也不会放心把具有一定深度的任务交给他。 第二,知识碎片化,不成系统。在面试中,面试者似乎无法完整、清晰地描述自己所开发的系统,或者使用的相关技术。
|
11月前
|
SQL 算法 NoSQL
2023年阿里高频Java面试题:分布式+中间件+高并发+算法+数据库
又到了一年一度的金九银十,互联网行业竞争是一年比一年严峻,作为工程师的我们唯有不停地学习,不断的提升自己才能保证自己的核心竞争力从而拿到更好的薪水,进入心仪的企业(阿里、字节、美团、腾讯.....)
|
12月前
|
存储 缓存 人工智能
2022互联网寒冬,看看阿里中间件团队如何降本提效?(2)
2022互联网寒冬,看看阿里中间件团队如何降本提效?
201 1