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 .