Введение в API-шлюзы
В мире микросервисов управление множеством бэкенд-API может стать сложной задачей. Здесь на помощь приходят API-шлюзы, которые действуют как единая точка входа для ваших API, упрощая взаимодействие с клиентами и снимая нагрузку по маршрутизации с ваших серверных служб. В этой статье мы отправимся в путешествие по созданию высокопроизводительного API-шлюза с использованием Go, уделяя особое внимание ключевым функциям, таким как регистрация сервисов, обратный проксинг, ограничение скорости и дополнительная авторизация.
Почему именно Go?
Go, или Golang, является отличным выбором для создания масштабируемых систем благодаря своим встроенным функциям параллелизма, надёжной стандартной библиотеке и простоте использования. Он быстро компилируется и создаёт легковесные двоичные файлы, что делает его идеальным для разработки микросервисов. Такие компании, как Netflix и Dropbox, уже использовали эффективность Go для своих сервисов.
Компоненты API-шлюза
Для создания нашего API-шлюза мы будем использовать следующие компоненты:
- Go: как язык программирования.
- Gorilla/Mux: для маршрутизации.
- Viper: для управления конфигурацией.
- Net/Http: для обработки HTTP-запросов.
Структура проекта
Наш проект будет иметь следующую структуру:
project/
|--- main.go
|--- config.yaml
|--- config.go
|--- services/
|--- service1.go
|--- service2.go
- main.go: обрабатывает основные функции, такие как маршрутизация и регистрация сервисов.
- config.yaml: определяет важные параметры конфигурации, такие как ограничения скорости и сведения о сервисах.
- config.go: управляет конфигурацией с помощью Viper.
Реализация API-шлюза
Шаг 1: настройка проекта
Сначала давайте инициализируем наш проект Go и установим необходимые пакеты:
go mod init api-gateway
go get github.com/gorilla/mux
go get github.com/spf13/viper
Шаг 2: управление конфигурацией
Мы будем использовать Viper для управления нашей конфигурацией. Вот как вы можете определить свой config.yaml
:
rateLimitWindow: 5m
rateLimitCount: 10
services:
service1:
base_url: https://your-backend-service1.com
routes:
- path: /api/v1/data
service2:
base_url: https://your-backend-service2.com
routes:
- path: /api/v2/data
И вот как вы можете прочитать эту конфигурацию в config.go
:
package config
import (
"fmt"
"log"
"github.com/spf13/viper"
)
type ServiceConfig struct {
BaseURL string `yaml:"base_url"`
Routes []string `yaml:"routes"`
}
type Config struct {
RateLimitWindow string `yaml:"rateLimitWindow"`
RateLimitCount int `yaml:"rateLimitCount"`
Services map[string]ServiceConfig `yaml:"services"`
}
func LoadConfig() (*Config, error) {
viper.SetConfigFile("config.yaml")
err := viper.ReadInConfig()
if err != nil {
return nil, err
}
var cfg Config
err = viper.Unmarshal(&cfg)
if err != nil {
return nil, err
}
return &cfg, nil
}
Шаг 3: маршрутизация и обратный прокси
Теперь настроим маршрутизацию и обратный прокси-сервер с помощью Gorilla/Mux. Вот фрагмент из main.go
:
package main
import (
"log"
"net/http"
"net/url"
"github.com/gorilla/mux"
"github.com/spf13/viper"
"your-project/config"
)
func main() {
cfg, err := config.LoadConfig()
if err != nil {
log.Fatal(err)
}
r := mux.NewRouter()
for serviceName, service := range cfg.Services {
for _, route := range service.Routes {
r.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
targetURL, err := url.Parse(service.BaseURL + r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
proxy := http.NewSingleHostReverseProxy(targetURL)
proxy.ServeHTTP(w, r)
})
}
}
log.Fatal(http.ListenAndServe(":8080", r))
}
Шаг 4: реализация ограничения скорости
Чтобы предотвратить злоупотребления, мы реализуем ограничение скорости. Мы можем использовать простую карту памяти для отслеживания запросов, но для производства рассмотрите возможность использования Redis или другого распределённого хранилища.
package main
import (
"sync"
"time"
)
type RateLimiter struct {
mu sync.RWMutex
requests map[string]int
timestamps map[string]time.Time
}
func NewRateLimiter() *RateLimiter {
return &RateLimiter{
requests: make(map[string]int),
timestamps: make(map[string]time.Time),
}
}
func (r *RateLimiter) Allow(ip string, limit int, window time.Duration) bool {
r.mu.Lock()
defer r.mu.Unlock()
now := time.Now()
if timestamp, ok := r.timestamps[ip]; ok && now.Before(timestamp.Add(window)) {
if r.requests[ip] >= limit {
return false
}
r.requests[ip]++
} else {
r.requests[ip] = 1
r.timestamps[ip] = now
}
return true
}
func main() {
// ...
limiter := NewRateLimiter()
r.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
if !limiter.Allow(r.RemoteAddr, cfg.RateLimitCount, time.Duration(cfg.RateLimitWindow)) {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
// ... остальная часть обработчика
})
// ...
}
Шаг 5: дополнительная авторизация
Для авторизации вы можете реализовать проверку JWT или другой механизм аутентификации. Вот базовый пример с использованием JWT:
package main
import (
"encoding/json"
"errors"
"net/http"
"github.com/golang-jwt/jwt/v4"
)
var jwtKey = []byte("your-secret-key")
func authenticate(w http.ResponseWriter, r *http.Request) error {
tokenString := r.Header.Get("Authorization")
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
return jwtKey, nil
})
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return nil
}
return errors.New("invalid token")
}
type Claims struct {
Username string `json:"username"`
jwt.RegisteredClaims
}
func main() {
// ...
r.HandleFunc(route, func(w http.ResponseWriter, r *http.Request) {
if err := authenticate(w, r); err != nil {
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
// ... остальной обработчик
})
// ...
}