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

Теория: HTTP Middleware

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

Чтобы решить эту проблему, в веб-приложениях используются посредники, которые выполняются перед обработчиками 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 позволяет группировать обработчиков, чтобы один раз описывать всех посредников для группы обработчиков
  • Посредники могут быть использованы для логирования запросов, проверки доступа, ограничения количества запросов

Рекомендуемые программы