Version: 2.2.8

Traefik comes with a lot of plugins by default, but some of our personal needs may not be supported by native plugins, so you need to develop your own plugins. Traefik does not support plugins prior to version 2.3, so we need to modify the source code to add plugins.

Let’s add a token validation plug-in as a demonstration.

The plugin takes the token that the request added to the header, and then asks the backend service to verify that the token is correct.

Code changes

There are three things we need to change,

Add plug-in execution files

Add the plugin’s main logic file to the PKG/Middleware/Auth folder, which can be changed to suit your needs.

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")

  // Plug-in structure
	ta := &tokenAuth{
		address:             config.Address,
		next:                next,
		name:                name,
	}

	// Create an HTTP client that requests other services
	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(a) (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 verification failed! \ "}")

  // Get the token from the header
	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
	}

  // All of the following are requests for other services to validate tokens

	// Build the request body
	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)
	iferr ! =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 request
	passportResponse, forwardErr := ta.client.Do(passportReq)
	ifforwardErr ! =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))

	/ / read the body
	body, readError := ioutil.ReadAll(passportResponse.Body)
	ifreadError ! =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()

	ifpassportResponse.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)
	iferr ! =nil {
		logMessage := fmt.Sprintf("Body unmarshal error. Body: %s", body)
		traceAndResponseError(logger, rw, req, logMessage, errorMsg, http.StatusBadRequest)
		return
	}

	// Check the return value. Non-0 indicates validation failure
	ifcommonRes.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)
}
Copy the code

Add a dynamic configuration mapping

Add the mapping between the configuration file and the entity here

// pkg/config/dynamic/middlewares.go

package dynamic

/ *... * /

// Middleware holds the Middleware configuration.
type Middleware struct {
  / *... * /

  // 
	TokenAuth         *TokenAuth         `json:"tokenAuth,omitempty" toml:"tokenAuth,omitempty" yaml:"tokenAuth,omitempty"`
}

/ *... * /

// TokenAuth
type TokenAuth struct {
	Address             string     `json:"address,omitempty" toml:"address,omitempty" yaml:"address,omitempty"`
}
Copy the code

Constructing a plug-in example

Here is the code to create the plug-in entity

// pkg/server/middleware/middlewares.go

func (b *Builder) buildConstructor(ctx context.Context, middlewareName string) (alice.Constructor, error) {
	/ *... * /

	// TokenAuth
	ifconfig.TokenAuth ! =nil {
		ifmiddleware ! =nil {
			return nil, badConf
		}
		middleware = func(next http.Handler) (http.Handler, error) {
			return auth.NewToken(ctx, next, *config.TokenAuth, middlewareName)
		}
	}

	/ *... * /
}
Copy the code

Packaging configuration

Use your own package command to package Linux (docker installation required).

make binary
Copy the code

The executable file is then generated under the dist folder.

Adding plug-in Configuration

HTTP: middlewares: # token validation token-auth: tokenAuth: address: <http://xxx.xxx.com/token_info>Copy the code

Added dynamic route configuration

http:
  routers:
    svc:
      entryPoints:
      - web
      middlewares:
      - token-auth
      service: svc
      rule: PathPrefix(`/list`)
Copy the code

This will make the newly added plug-in available.