JWT в Fiber

Промежуточное ПО JWT возвращает промежуточное программное обеспечение аутентификации JSON Web Token (JWT). При наличии действительного токена оно устанавливает пользователя в Ctx.Locals и вызывает следующий обработчик. При недействительном токене возвращается ошибка "401 - Неавторизован". При отсутствии токена - ошибка "400 - Неверный запрос".

Примечание: Требуется Go версии 1.19 или выше

Установка

Это промежуточное ПО поддерживает Fiber v1 и v2, установите соответственно.

go get -u github.com/gofiber/fiber/v2
go get -u github.com/gofiber/contrib/jwt
go get -u github.com/golang-jwt/jwt/v5

Подпись

jwtware.New(config ...jwtware.Config) func(*fiber.Ctx) error

Конфигурация

Свойство Тип Описание Значение по умолчанию
Filter func(*fiber.Ctx) bool Определяет функцию для пропуска промежуточного ПО nil
SuccessHandler func(*fiber.Ctx) error Определяет функцию для выполнения при действительном токене nil
ErrorHandler func(*fiber.Ctx, error) error Определяет функцию для выполнения при недействительном токене 401 Недействительный или истекший JWT
SigningKey interface{} Ключ подписи, используемый для проверки токена. Если длина SigningKeys равна 0, он используется в качестве запасного варианта nil
SigningKeys map[string]interface{} Соответствие используемых ключей подписи для проверки токенов с полем kid nil
ContextKey string Ключ контекста, используемый для хранения информации о пользователе из токена в контексте "user"
Claims jwt.Claim Расширенные данные утверждений, определяющие содержимое токена jwt.MapClaims{}
TokenLookup string Строка в формате :, используемая для разбора токена "header:Authorization"
AuthScheme string Используемая AuthScheme в заголовке Authorization. Значение по умолчанию ("Bearer") используется только с значением TokenLookup по умолчанию "Bearer"
KeyFunc func() jwt.Keyfunc Пользовательская функция предоставления открытого ключа для проверки токена jwtKeyFunc
JWKSetURLs []string Массив уникальных URL-адресов JSON Web Key (JWK) Set, используемых для разбора JWT nil

Пример использования HS256

package main

import (
	"time"

	"github.com/gofiber/fiber/v2"

	jwtware "github.com/gofiber/contrib/jwt"
	"github.com/golang-jwt/jwt/v5"
)

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

	// Маршрут входа
	app.Post("/login", login)

	// Доступный маршрут без аутентификации
	app.Get("/", accessible)

	// Промежуточное ПО JWT
	app.Use(jwtware.New(jwtware.Config{
		SigningKey: jwtware.SigningKey{Key: []byte("секрет")},
	}))

	// Ограниченный маршрут
	app.Get("/restricted", restricted)

	app.Listen(":3000")
}

func login(c *fiber.Ctx) error {
	user := c.FormValue("user")
	pass := c.FormValue("pass")

	// Генерация ошибки неавторизованного доступа
	if user != "john" || pass != "doe" {
		return c.SendStatus(fiber.StatusUnauthorized)
	}

	// Создание утверждений
	claims := jwt.MapClaims{
		"name":  "John Doe",
		"admin": true,
		"exp":   time.Now().Add(time.Hour * 72).Unix(),
	}

	// Создание токена
	token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

	// Генерирование закодированного токена и отправка в качестве ответа
	t, err := token.SignedString([]byte("секрет"))
	if err != nil {
		return c.SendStatus(fiber.StatusInternalServerError)
	}

	return c.JSON(fiber.Map{"token": t})
}

func accessible(c *fiber.Ctx) error {
	return c.SendString("Доступно")
}

func restricted(c *fiber.Ctx) error {
	user := c.Locals("user").(*jwt.Token)
	claims := user.Claims.(jwt.MapClaims)
	name := claims["name"].(string)
	return c.SendString("Добро пожаловать, " + name)
}

Тест HS256

Войдите с использованием имени пользователя и пароля, чтобы получить токен.

curl --data "user=john&pass=doe" http://localhost:3000/login

Ответ

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"
}

Запрос ограниченного ресурса с использованием токена в заголовке авторизации.

curl localhost:3000/restricted -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NjE5NTcxMzZ9.RB3arc4-OyzASAaUhC2W3ReWaXAt_z2Fd3BN4aWTgEY"

Ответ

Добро пожаловать John Doe
package main

import (
	"crypto/rand"
	"crypto/rsa"
	"log"
	"time"

	"github.com/gofiber/fiber/v2"

	"github.com/golang-jwt/jwt/v5"

	jwtware "github.com/gofiber/contrib/jwt"
)

var (
	// Очевидно, что это всего лишь тестовый пример. Не используйте это в продакшене.
	// В продакшене вы должны заранее создавать пару ключей.
	// Никогда не добавляйте приватный ключ в репозиторий GitHub.
	privateKey *rsa.PrivateKey
)

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

	// Для демонстрации создается новая пара закрытого/открытого ключа при каждом запуске программы. См. примечание выше.
	rng := rand.Reader
	var err error
	privateKey, err = rsa.GenerateKey(rng, 2048)
	if err != nil {
		log.Fatalf("rsa.GenerateKey:%v", err)
	}

	// Маршрут для входа
	app.Post("/login", login)

	// Маршрут для незарегистрированных пользователей
	app.Get("/", accessible)

	// Промежуточное ПО JWT
	app.Use(jwtware.New(jwtware.Config{
		SigningKey: jwtware.SigningKey{
			JWTAlg: jwtware.RS256,
			Key:    privateKey.Public(),
		},
	}))

	// Ограниченный маршрут
	app.Get("/restricted", restricted)

	app.Listen(":3000")
}

func login(c *fiber.Ctx) error {
	user := c.FormValue("user")
	pass := c.FormValue("pass")

	// Прекратить работу из-за отказа в авторизации
	if user != "john" || pass != "doe" {
		return c.SendStatus(fiber.StatusUnauthorized)
	}

	// Создать утверждения
	claims := jwt.MapClaims{
		"name":  "John Doe",
		"admin": true,
		"exp":   time.Now().Add(time.Hour * 72).Unix(),
	}

	// Создать токен
	token := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)

	// Создать закодированный токен и отправить его в качестве ответа
	t, err := token.SignedString(privateKey)
	if err != nil {
		log.Printf("token.SignedString:%v", err)
		return c.SendStatus(fiber.StatusInternalServerError)
	}

	return c.JSON(fiber.Map{"token": t})
}

func accessible(c *fiber.Ctx) error {
	return c.SendString("Доступно")
}

func restricted(c *fiber.Ctx) error {
	user := c.Locals("user").(*jwt.Token)
	claims := user.Claims.(jwt.MapClaims)
	name := claims["name"].(string)
	return c.SendString("Добро пожаловать " + name)
}

Тест RS256

RS256 по сути то же самое, что и тест HS256 выше.

Тест JWK Set

Эти тесты такие же, как и основные тесты JWT выше, но они требуют действительный набор открытых ключей в формате JWK Set по адресам JWKSetURLs.

Пример пользовательской функции KeyFunc

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

При предоставлении пользовательской функции KeyFunc параметры SigningKey, SigningKeys и SigningMethod будут проигнорированы. Это один из трех вариантов предоставления ключей для проверки токена. Приоритетный порядок предоставления: пользовательская функция KeyFunc, SigningKeys и SigningKey. Если ни параметры SigningKeys, ни SigningKey не были предоставлены, эта функция должна быть обязательно предоставлена. По умолчанию проверка алгоритма подписи и выбор соответствующего ключа выполняется внутренней реализацией.

package main

import (
	"fmt"
	"github.com/gofiber/fiber/v2"
	
	jwtware "github.com/gofiber/contrib/jwt"
	"github.com/golang-jwt/jwt/v5"
)

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

	app.Use(jwtware.New(jwtware.Config{
		KeyFunc: customKeyFunc(),
	}))

	app.Get("/ok", func(c *fiber.Ctx) error {
		return c.SendString("OK")
	})
}

func customKeyFunc() jwt.Keyfunc {
	return func(t *jwt.Token) (interface{}, error) {
		// Проверяем, что метод подписи верный
		if t.Method.Alg() != jwtware.HS256 {
			return nil, fmt.Errorf("Неожиданный метод подписи JWT=%v", t.Header["alg"])
		}

		// TODO Реализовать загрузку пользовательского ключа подписи, например, загрузка из базы данных
    	signingKey := "секрет"

		return []byte(signingKey), nil
	}
}