Go: RESTful - exemplo com persistência, gorm, e segurança

De Aulas

Afluentes: Sistemas Distribuídos e Mobile

Segurança de que?

Sim, é importante sempre cuidar a segurança do uso da nossa api. O GORM já faz a parte dele, se bem utilizado, prevenindo sql injection, mas só isso pode não bastar. Sua api pode ter um ataque com muitas conexões simultâneas, sobrecarregando o servidor. Ou pode ser ataque ou até uma má implementação do front-end que consome a api.

No exemplo abaixo temos os seguintes recursos de segurança:

  • GORM: evitando sql injection;
  • API-KEY: validação via par X-User e X-API-KEY, ou seja, o usuário e uma chave de acesso à api.
  • Limitação de taxa de acesso: limitador da quantidade de acessos que cada cliente faz por um segundo.
  • Limitação de conexões: quantas conexões podem estar abertas ao mesmo tempo.

Segue o exemplo:

package main

import (
	"encoding/json"
	"fmt"
	"html/template"
	"io"
	"log"
	"net/http"
	"os"
	"sync/atomic"

	"github.com/rs/cors"
	"golang.org/x/time/rate"

	"gorm.io/driver/postgres"
	"gorm.io/gorm"
)

var (
	// Quantidade máxima de requisições simultâneas
	maxConnections int64 = 100
	// Número de conexões abertas
	currentConnections int64 = 0
	// 1 requisição por segundo com um burst de 5
	limiter = rate.NewLimiter(1, 5)
	// Arquivo com as chaves, podia ser em Banco de Dados
	filePath = "apikey.json"
)

// Definição do esquema User no banco de dados
type User struct {
	Id    uint   `gorm:"primaryKey" json:"id"`
	Name  string `gorm:"type:varchar(50)" json:"name"`
	Email string `gorm:"type:varchar(50)" json:"email"`
}

// Estrutura de Usuário e apikey
type ApiKeyPair struct {
	Username string `json:"username"`
	Apikey   string `json:"apikey"`
}

// Função que trata o limite da taxa de acessos
func rateLimiter(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if !limiter.Allow() {
			http.Error(w, "Too many requests", http.StatusTooManyRequests)
			return
		}
		next.ServeHTTP(w, r)
	})
}

// Função que trata o limite da quantidade de conexões simultâneas
func connectionsLimiter(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		if atomic.AddInt64(&currentConnections, 1) > maxConnections {
			atomic.AddInt64(&currentConnections, -1)
			http.Error(w, "Too many connections", http.StatusTooManyRequests)
			return
		}
		defer atomic.AddInt64(&currentConnections, -1)
		next.ServeHTTP(w, r)
	})
}

// Função para validar a API key
func apiKeyValidation(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		user := r.Header.Get("X-User")
		key := r.Header.Get("X-API-KEY")
		isValid, _ := isValidUser(user, key, filePath)
		if !isValid {
			http.Error(w, "Forbidden", http.StatusForbidden)
			return
		}
		next.ServeHTTP(w, r)
	})
}

/*
Função para verificar se o usuário e a chave existem no arquivo.
Aqui poderia ser feito acesso à uma tabela no banco de dados com
o cadastro dos usuários e suas respectivas apikeys.
*/
func isValidUser(xuser, xapiKey, filePath string) (bool, error) {
	// Ler o arquivo JSON
	file, err := os.Open(filePath)
	if err != nil {
		return false, err
	}
	defer file.Close()

	// Parsear o conteúdo do arquivo JSON
	byteValue, _ := io.ReadAll(file)
	var users []ApiKeyPair
	json.Unmarshal(byteValue, &users)

	// Verificar se o usuário e a chave existem
	for _, u := range users {
		if u.Username == xuser && u.Apikey == xapiKey {
			return true, nil
		}
	}
	return false, nil
}

func main() {
	// String de conexão para PostgreSQL
	dsn := "host=localhost user=saulo password=1234 dbname=usersapi sslmode=disable"

	// Abrir conexão com o banco de dados PostgreSQL
	db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
	if err != nil {
		log.Fatal(err)
	}

	// Migrar a tabela, criando ela, caso não exista
	if err := db.AutoMigrate(&User{}); err != nil {
		log.Fatal(err)
	}

	router := http.NewServeMux()

	/*
		Nesse exemplo vamos colocar uma página na raiz da API. Podemos colocar uma
		página de apresentação ou até um manual para a utilização da API. No nosso
		caso, apenas uma página de apresentação.

		Também estamos usando uma biblioteca de templates. Criamos um arquivo HTML
		e nele colocamos elementos especiais para depois substituir por informações
		colocadas via código. Aqui estamos apenas tratando das informações Title e
		Message. Veja como fica no documento HTML. Lá colocamos os nomes dos elementos
		no formato {{.NomeDoElemento}}
	*/
	router.HandleFunc("GET /", func(w http.ResponseWriter, r *http.Request) {
		// Cria o template com base no arquivo index.html
		tmpl := template.Must(template.ParseFiles("index.html"))
		data := struct {
			Title   string
			Message string
		}{
			Title:   "Arisa API",
			Message: "Bem vindo a API da Arisa",
		}
		// Renderiza o template
		if err := tmpl.Execute(w, data); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
		}
	})

	router.HandleFunc("POST /users", func(w http.ResponseWriter, r *http.Request) {
		var user User
		err := json.NewDecoder(r.Body).Decode(&user)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		if err := db.Create(&user).Error; err != nil {
			log.Println(err) // Registra o erro
			http.Error(w, "Error creating user", http.StatusInternalServerError)
			return
		}
	})

	router.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		var users []User
		if err := db.Find(&users).Error; err != nil {
			log.Println(err) // Registra o erro
			http.Error(w, "Error creating user", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode(users)
	})

	router.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		id := r.PathValue("id")
		var user User
		if err := db.First(&user, id).Error; err != nil { // buscar pelo Id
			log.Println(err) // Registra o erro
			http.Error(w, "Error creating user", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode(user)
	})

	router.HandleFunc("PUT /users", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		var user User
		err := json.NewDecoder(r.Body).Decode(&user)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		newUser := User{Name: user.Name, Email: user.Email}
		if err := db.Model(&User{}).Where("id = ?", user.Id).Updates(newUser).Error; err != nil {
			log.Println(err) // Registra o erro
			http.Error(w, "Error creating user", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode("ok")
	})

	router.HandleFunc("DELETE /users/{id}", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json;charset=UTF-8")
		id := r.PathValue("id")
		var user User
		if err := db.Delete(&user, id).Error; err != nil {
			log.Println(err) // Registra o erro
			http.Error(w, "Error creating user", http.StatusInternalServerError)
			return
		}
		_ = json.NewEncoder(w).Encode("ok")
	})

	c := cors.AllowAll()

	/*
		Nessa linha executamos vários tratamentos. São eles:
		1. Validação da apikey
		2. Aplicação do tratamento da limitação das conexões
		3. Aplicação do tratamento da taxa de conexões
		4. Liberação do cors
	*/
	usersHandler := c.Handler(connectionsLimiter(rateLimiter(apiKeyValidation(router))))

	// No raiz não precisa verificar a API-KEY, nem precisa de tratamento do cors
	rootHandler := connectionsLimiter(rateLimiter(router))

	// Aplica o tratamento da rota raiz
	http.Handle("/", rootHandler)

	// Aplica o tratamento das operações usuários
	http.Handle("/users", usersHandler)

	err = http.ListenAndServe("localhost:8080", nil)
	if err != nil {
		fmt.Println(err.Error())
	}
}