Enriquecendo requests com Traefik
Matheus Mina
Posted on October 13, 2022
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.
É 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
}
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:
Bem legal, né? Para resumir tudo, o ciclo da requisição vai funcionar assim:
- O usuário faz a requisição ao backend
- O Traefik recebe a requisição e segura a requisição original
- O Traefik faz uma nova requisição ao middleware
- O Traefik pega a resposta do middleware e adiciona o header configurado na request original.
- O Traefik encaminha a request original ao serviço backend
- 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
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
}
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"
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
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
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)
}
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.
Posted on October 13, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.