DEV Community

Matheus Mina
Matheus Mina

Posted on

Enriquecendo requests com Traefik

Atualmente grande parte dos fluxos de autenticação se baseia em gerar um token, que pode por exemplo usar o padrão JWT, e o frontend faz as requisições informando ao backend quem é o usuário que está de fato realizando a chamada. Isso pode ser observado com as requests do frontend enviando o header Authorization nas requests.

Exemplo do Header Authorization

É comum que esse token contenha informações do usuário, como por exemplo o id. Então ao receber a requisição, o backend decodifica esse token para extrair essas informações e assim relacionar com algum usuário do banco de dados. Com o usuário em mãos, executamos a ação desejada. Logo abaixou vou dar um exemplo de um serviço em Go que faz exatamente isso.

package service

import (
    "encoding/json"
    "errors"
    "fmt"
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt"
)

type User struct {
    ID    string `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email"`
}

func main() {
    http.HandleFunc("/", getEmail)

    fmt.Println("Listening on :8081")
    err := http.ListenAndServe(":8081", nil)
    if err != nil {
        fmt.Println(err)
    }
}

func getEmail(w http.ResponseWriter, r *http.Request) {
    // example from https://pkg.go.dev/github.com/golang-jwt/jwt/v4@v4.4.2
    authHeader := r.Header.Get("Authorization")
    tokenStr, err := getToken(authHeader)
    if err != nil {
        fmt.Println(err)
    }

    token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }

        return []byte("my_secret_key"), nil
    })

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || !token.Valid {
        fmt.Println(err)
    }

    // get the user from the ID
    // user := users.GetUserById(claims["user_id"])
    userID := claims["user_id"].(string)
    user := User{ID: userID, Name: "Matheus Mina", Email: "mfbmina@gmail.com"}
    userJSON, _ := json.Marshal(user)
    w.Write(userJSON)
}

func getToken(tokenStr string) (string, error) {
    authHeaderParts := strings.Fields(tokenStr)
    if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
        return "", errors.New("Not valid")
    }

    return authHeaderParts[1], nil
}
Enter fullscreen mode Exit fullscreen mode

Esse fluxo funciona muito bem para monolitos, mas para micro-serviços não. O problema é que ao ir para um ambiente de micro-serviços, essa lógica responsável por abrir o token tem que ser replicada para cada um dos serviços novos. Se por acaso o formato do token mudar, todos os micro-serviços vão ter que se atualizar para seguir o padrão novo de token.

Para evitar esse problema, podemos fazer algo chamado de enriquecimento de requests. Isso consiste em adicionar mais informações a request original, dando mais contexto e informações ao backend. Um serviço que faz isso, por exemplo, é o Cloudflare que adiciona alguns headers na sua requisição. Para fazer esse enriquecimento, podemos colocar uma aplicação intermediaria para fazer essa abertura de token e colocar a requisição no header das demais respostas.

Uma maneira bem simples de fazer isso é utilizar os mecanismos de middleware do Traefik. Ele é um proxy reverso e load balancer que nos permite de maneira simples fazer roteamento entre os nossos microserviços. Além disso, ele é open-source e escrito em Go. Utilizando a idéia do middleware com o Traefik, a nossa arquitetura ficaria assim:

Fluxo da solução final

Bem legal, né? Para resumir tudo, o ciclo da requisição vai funcionar assim:

  1. O usuário faz a requisição ao backend
  2. O Traefik recebe a requisição e segura a requisição original
  3. O Traefik faz uma nova requisição ao middleware
  4. O Traefik pega a resposta do middleware e adiciona o header configurado na request original.
  5. O Traefik encaminha a request original ao serviço backend
  6. O serviço responde o usuário

Colocando a mão na massa, vamos configurar nosso serviço no Traefik receber e encaminhar as chamadas para o nosso serviço.

entryPoints:
  web:
    # Listen on port 8081 for incoming requests
    address: :8081

providers:
  # Enable the file provider to define routers / middlewares / services in file
  file:
    directory: /path/to/dynamic/conf

# dynamic config below
http:
  routers:
    # Define a connection between requests and services
    user-service:
      rule: "Path(`/users`)"
      service: user-service

  services:
    # Define how to reach an existing service on our infrastructure
    user-service:
      loadBalancer:
        servers:
        - url: http://private/user-service
Enter fullscreen mode Exit fullscreen mode

Com essa configuração, todo request para /users vai ir para o nosso UserService. A idéia aqui é adicionar um middleware no meio, de forma que seja transparente para o usuário que o token está sendo aberto. Para isso vamos criar um outro microserviço cuja responsabilidade seja só abrir esse token e enriquecer essa request.

package middleware

import (
    "errors"
    "fmt"
    "log"
    "net/http"
    "strings"

    "github.com/golang-jwt/jwt"
)

func main() {
    http.HandleFunc("/", setHeaderExample)

    fmt.Println("Listening on :8082")
    err := http.ListenAndServe(":8082", nil)
    if err != nil {
        log.Fatal(err)
    }
}

func setHeaderExample(w http.ResponseWriter, r *http.Request) {
    // example from https://pkg.go.dev/github.com/golang-jwt/jwt/v4@v4.4.2
    authHeader := r.Header.Get("Authorization")
    tokenStr, err := getToken(authHeader)
    if err != nil {
        fmt.Println(err)
    }

    token, err := jwt.Parse(tokenStr, func(token *jwt.Token) (interface{}, error) {
        if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
            return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
        }

        return []byte("my_secret_key"), nil
    })

    claims, ok := token.Claims.(jwt.MapClaims)
    if !ok || !token.Valid {
        fmt.Println(err)
    }

    // Setting the header X-User-Id"
    userID := claims["user_id"].(string)
    w.Header().Add("X-User-Id", userID)
    w.Write([]byte("This response has the X-User-Id header"))
}

func getToken(tokenStr string) (string, error) {
    authHeaderParts := strings.Fields(tokenStr)
    if len(authHeaderParts) != 2 || strings.ToLower(authHeaderParts[0]) != "bearer" {
        return "", errors.New("Not valid")
    }

    return authHeaderParts[1], nil
}
Enter fullscreen mode Exit fullscreen mode

Dessa forma, é necessário adicionar o mesmo como um middleware no Traefik:

http:
  services:
    # Define how to reach an existing service on our infrastructure
    user-middleware:
      loadBalancer:
        servers:
        - url: http://private/user-middleware  

    middlewares:
    user-middleware:
      forwardAuth:
        address: "http://private/user-middleware"
        authResponseHeaders:
          - "X-User-ID"
Enter fullscreen mode Exit fullscreen mode

Também vamos dizer para o UserService utilizar o middleware:

http:
  routers:
    # Define a connection between requests and services
    user-service:
      rule: "Path(`/users`)"
      service: user-service
            middlewares:
      - user-middleware
Enter fullscreen mode Exit fullscreen mode

A configuração completa fica da seguinte forma:

entryPoints:
  web:
    # Listen on port 8081 for incoming requests
    address: :8081

providers:
  # Enable the file provider to define routers / middlewares / services in file
  file:
    directory: /path/to/dynamic/conf

# dynamic config below
http:
  routers:
    # Define a connection between requests and services
    user-service:
      rule: "Path(`/users`)"
      service: user-service
            middlewares:
      - user-middleware
    middlewares:
    user-middleware:
      forwardAuth:
        address: "http://private/user-middleware"
        authResponseHeaders:
          - "X-User-ID"
  services:
    # Define how to reach an existing service on our infrastructure
    user-service:
      loadBalancer:
        servers:
        - url: http://private/user-service
        user-middleware:
      loadBalancer:
        servers:
        - url: http://private/user-middleware  
Enter fullscreen mode Exit fullscreen mode

E pronto! Mágica funcionando! Todas as requests pro UserService vão ter o header X-User-Id. Para finalizar, é só a gente remover o código que “abre” o token e passar a ler a informação vinda do header. Nosso handler ficaria assim:

func getEmailFinal(w http.ResponseWriter, r *http.Request) {
    // get the user from the ID
    // user := users.GetUserById(claims["user_id"])
    userID := r.Header.Get("X-User-Id")
    user := User{ID: userID, Name: "Matheus Mina", Email: "mfbmina@gmail.com"}
    userJSON, _ := json.Marshal(user)
    w.Write(userJSON)
}
Enter fullscreen mode Exit fullscreen mode

Ao enriquecer a request podemos simplificar o código dos nossos serviços, repassando informações utéis ao backend de forma transparente. Podemos ver que o handler do nosso serviço ficou bem mais limpo, focando somente no que ele de fato deveria fazer.

Se curtiu o post, você também pode me encontrar no Twitter, Github ou LinkedIn.

Top comments (0)