Введение в 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
        }
        // ... остальной обработчик
    })
    // ...
}

Диаграммы для наглядности

Последовательность действий: поток запросов

СервисШлюзКлиентСервисШлюзКлиентHTTP-запросПроверка ограничения скоростиАутентификация (необязательно)Пересылка запросаОтветОтвет

Диаграмма классов: компоненты

APIGateway

-config: Config

-limiter: RateLimiter

+handleRequest()