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(¤tConnections, 1) > maxConnections {
atomic.AddInt64(¤tConnections, -1)
http.Error(w, "Too many connections", http.StatusTooManyRequests)
return
}
defer atomic.AddInt64(¤tConnections, -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())
}
}