Зарегистрируйтесь для доступа к 15+ бесплатным курсам по программированию с тренажером

HTTP Middleware Веб-разработка на Go

В веб-приложениях часто есть логика, которая должна выполняться перед обработкой любого запроса. К такой логике относятся: проверка доступа, логирование запроса, ограничение количества запросов. Перед веб-разработчиками часто встает проблема в организации такой логики, чтобы разрабатывать без сложностей и поддерживать приложение.

Чтобы решить эту проблему, в веб-приложениях используются посредники, которые выполняются перед обработчиками HTTP-запросов. В этом уроке мы научимся использовать посредников в микрофреймворке Fiber и разберем конкретные примеры реализации.

Middlewares

Посредники или middlewares в Fiber являются функциями func(c *fiber.Ctx) error, которые мы устанавливаем перед обработчиками. Посредник может изменять запрос, добавлять заголовки, выполнять логирование. Посредник также может прервать цепочку обработки запроса, если возникла ошибка.

Пример посредника, который проверяет возможность доступа пользователя к веб-приложению, может выглядеть следующим образом:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/sirupsen/logrus"
)

func main() {
    webApp := fiber.New()
    webApp.Use(accessMiddleware)
    webApp.Post("/do/something", func(ctx *fiber.Ctx) error {
        ...
    })

    logrus.Fatal(webApp.Listen(":80"))
}

func accessMiddleware(c *fiber.Ctx) error {
    accessToken := c.Params("access_token")
    if !hasAccess(accessToken) {
        // Посредник прерывает цепочку обработки запроса.
        return c.SendStatus(fiber.StatusUnauthorized)
    }

    // Пользователь имеет доступ, продолжаем выполнение запроса.
    return c.Next()
}

В примере выше мы создали посредник accessMiddleware, который проверяет наличие корректного токена доступа в параметрах запроса. Если токен не прошел проверку, то посредник прерывает цепочку обработки запроса и возвращает клиенту статус 401 Unauthorized. Если токен валиден, то посредник вызывает метод c.Next(), который продолжает обработку запроса.

Такой подход работает, когда мы хотим использовать посредника на всех обработчиков в приложении. Но бывают ситуации, когда нам нужно использовать посредник только на некоторых обработчиках.

Группировка запросов в маршрутизации

Чтобы назначить посредника только на некоторые обработчики в Fiber, их нужно сначала сгруппировать с помощью функции r.Group(). После этого мы можем устанавливать посредников для всей группы с помощью r.Use():

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/sirupsen/logrus"
)

func main() {
    webApp := fiber.New()

    // Создаем группу с префиксом пути запроса "/authorized".
    authGroup := webApp.Group("/authorized")
    // Добавляем посредника проверки доступа в группу.
    authGroup.Use(accessMiddleware)
    // Добавляем обработчики запросов в группу.
    authGroup.Post("/action/1", func(ctx *fiber.Ctx) error {})
    authGroup.Post("/action/2", func(ctx *fiber.Ctx) error {})
    authGroup.Post("/action/3", func(ctx *fiber.Ctx) error {})

    // Создаем группу с префиксом пути запроса "/public".
    publicGroup := webApp.Group("/public")
    // У группы нет посредников.
    // При запросах к группе сразу выполняются обработчики.
    publicGroup.Post("/action/1", func(ctx *fiber.Ctx) error {})
    publicGroup.Post("/action/2", func(ctx *fiber.Ctx) error {})
    publicGroup.Post("/action/3", func(ctx *fiber.Ctx) error {})


    logrus.Fatal(webApp.Listen(":80"))
}

В примере выше мы создали две группы запросов: authGroup и publicGroup. В группу authGroup мы добавили посредник accessMiddleware, который проверяет наличие корректного токена доступа в параметрах запроса. В группу publicGroup мы не добавили посредников, поэтому при запросах к группе сразу выполняются обработчики:

set

Мы разобрались, как создавать своих посредников в Fiber. Но также существует набор готовых посредников, которые мы можем подключать в своих проектах. Каждый посредник представлен отдельным пакетом, который можно добавлять в проект при необходимости. Рассмотрим самые популярные из них.

Логирование запросов

Логирование необходимо для отладки работающих веб-приложений. Помимо явного логирования в местах ошибок, мы можем добавить логирование всех запросов. Так мы будем понимать, какие запросы приходят в приложение, по какому пути и с какими параметрами.

Чтобы добавить логирование всех запросов в Fiber-приложение, нужно подключить к проекту пакет github.com/gofiber/fiber/v2/middleware/logger и инициализировать его перед всеми обработчиками:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/sirupsen/logrus"
    "time"
)

func main() {
    webApp := fiber.New()

    webApp.Use(logger.New())
    webApp.Get("/", func(c *fiber.Ctx) error {
        // Создаем искусственную задержку, чтобы проверить логирование.
        time.Sleep(300 * time.Millisecond)

        return c.SendString("OK")
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и переходим в браузере на страницу http://localhost. Видим, что в ответ пришло сообщение «OK». При этом в консоли веб-приложения появилось сообщение о залогированном запросе:

12:59:24 | 200 |   301ms |       127.0.0.1 | GET     | /

Если несколько раз перезагружать страницу http://localhost, мы увидим несколько записей лога.

Так мы добавили логирование всех запросов в наше веб-приложение. По умолчанию логирование происходит в консоль, но можно настроить логирование в файл или в другие системы логирования.

Также при инициализации посредника для логирования мы можем указать формат логов, который подходит для нашего проекта. Для этого нужно передать в функцию инициализации посредника параметр logger.Config:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/sirupsen/logrus"
    "time"
)

func main() {
    webApp := fiber.New()

    webApp.Use(logger.New(logger.Config{
        Format:     "${time} ${method} ${path} - ${status} - ${latency}\n",
        TimeFormat: "2006-01-02 15:04:05.000000",
    }))
    webApp.Get("/", func(c *fiber.Ctx) error {
        // Создаем искусственную задержку, чтобы проверить логирование.
        time.Sleep(300 * time.Millisecond)

        return c.SendString("OK")
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и переходим в браузере на страницу http://localhost. Видим, что в ответ пришло сообщение «OK». При этом в консоли веб-приложения изменился формат логов:

2022-10-28 13:02:07.607325  GET      / -  200  -   301ms

Также хорошей практикой считается логировать идентификатор, который поможет нам связать все логи в рамках одного запроса. Для этого нам нужно подключить к проекту пакет github.com/gofiber/fiber/v2/middleware/requestid. И инициализировать его перед посредником для логирования:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/logger"
    "github.com/gofiber/fiber/v2/middleware/requestid"
    "github.com/sirupsen/logrus"
    "time"
)

func main() {
    webApp := fiber.New()

    webApp.Use(requestid.New())
    webApp.Use(logger.New(logger.Config{
        Format:     "${locals:requestid}: ${time} ${method} ${path} - ${status} - ${latency}\n",
        TimeFormat: "2006-01-02 15:04:05.000000",
    }))
    webApp.Get("/", func(c *fiber.Ctx) error {
        // Создаем искусственную задержку, чтобы проверить логирование.
        time.Sleep(300 * time.Millisecond)

        logrus.WithFields(logrus.Fields{
            "request_id": c.Locals("requestid"),
        }).Warn("something went wrong")

        return c.SendString("OK")
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и открываем в браузере страницу http://localhost. После этого проверяем консоль веб-приложения:

WARN[0001] something went wrong                          request_id=69b39a07-fea7-45a1-809d-3f3f1f897068
69b39a07-fea7-45a1-809d-3f3f1f897068: 2022-10-28 13:41:38.611714  GET      / -  200  -   300ms

Мы видим, что теперь в каждом логе есть идентификатор запроса. Если что-то пойдет не так, по идентификатору мы сможем понять, в рамках какого запроса происходили ошибки. Это поможет при отладке или исправлении ошибок в работающем веб-приложении.

Логирование запросов помогает нам разобраться в возникающих ошибках, но есть посредники, которые позволяют предотвратить новые ошибки. Один из примеров таких посредников — ограничение количества запросов в единицу времени.

Ограничение количества запросов

Представим, что у нас есть небольшое веб-приложение, у которого сто клиентов, и все работает бесперебойно. Конкурент завидует нашему успеху и решает написать скрипт, который будет отправлять миллионы запросов в наше приложение. Цель конкурента — вывести наши сервера из работы, чтобы клиенты не могли пользоваться приложением, и бизнес терял прибыль.

Чтобы защититься со стороны веб-приложения от таких атак, следует настроить ограничение количества запросов — throttling. Для этого мы будем использовать пакет github.com/gofiber/fiber/v2/middleware/limiter. Когда пакет подключен, мы можем настроить нового посредника, чтобы ограничить количество запросов с одного IP-адреса:

package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/middleware/limiter"
    "github.com/sirupsen/logrus"
    "time"
)

func main() {
    webApp := fiber.New()

    webApp.Use(limiter.New(limiter.Config{
        KeyGenerator: func(c *fiber.Ctx) string {
            return c.IP()
        },
        Max:        3,
        Expiration: 10 * time.Second,
    }))
    webApp.Get("/", func(c *fiber.Ctx) error {
        return c.SendString("OK")
    })

    logrus.Fatal(webApp.Listen(":80"))
}

Запускаем веб-приложение и открываем в браузере страницу http://localhost четыре раза. На первые три запроса мы получаем ответ «OK», но на четвертый получаем ответ "Too Many Requests".

С помощью структуры limiter.Config мы установили максимальное количество запросов в единицу времени. В нашем случае это три запроса в десять секунд. Если мы превысим это количество, то наш запрос будет отклонен с кодом 429 (Too Many Requests), что мы и увидели на четвертый запрос. По истечении десяти секунд с момента первого запроса мы снова сможем открывать страницу и получать успешный ответ.

В данном примере мы использовали IP-адрес клиента, чтобы определить источник запроса. Но можно использовать любой другой идентификатор, например, идентификатор пользователя или сессии.

Выводы

  • Посредники позволяют выполнять действия до или после обработки запроса
  • Fiber позволяет группировать обработчиков, чтобы один раз описывать всех посредников для группы обработчиков
  • Посредники могут быть использованы для логирования запросов, проверки доступа, ограничения количества запросов

Дополнительные материалы

  1. Fiber Middleware
  2. Fiber Request ID Middleware
  3. Fiber Logging Middleware
  4. Fiber Limiter Middleware

Аватары экспертов Хекслета

Остались вопросы? Задайте их в разделе «Обсуждение»

Вам ответят команда поддержки Хекслета или другие студенты

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff

Используйте Хекслет по-максимуму!

  • Задавайте вопросы по уроку
  • Проверяйте знания в квизах
  • Проходите практику прямо в браузере
  • Отслеживайте свой прогресс

Зарегистрируйтесь или войдите в свой аккаунт

Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»