Введение в JWT

JWT (JSON Web Token) является компактным, автономным методом аутентификации в сети. Он состоит из трех частей: заголовка, полезной нагрузки и подписи. В заголовке содержится информация о типе токена и алгоритме шифрования, полезная нагрузка содержит передаваемые данные (обычно включая информацию о разрешениях и идентификации пользователя), а подпись используется для проверки целостности и действительности токена.

JWT в основном используется в двух сценариях:

  1. Аутентификация: После входа пользователя каждый последующий запрос будет включать JWT, позволяя пользователю получить доступ к разрешенным маршрутам, услугам и ресурсам.
  2. Обмен информацией: JWT хорошо подходит для безопасной передачи информации между сторонами, так как они могут быть цифрово подписаны, например, с использованием пар ключей открытого/закрытого типа.

Формат данных JWT

JWT является компактным, URL-безопасным способом представления информации, чтобы ее можно было обменивать между сторонами. Фактически JWT состоит из трех частей, разделенных точками (.), а именно Header.Payload.Signature. Далее мы подробно рассмотрим формат данных этих трех частей.

1. Заголовок

Заголовок обычно состоит из двух частей: типа токена — обычно JWT и используемого алгоритма подписи или шифрования, такого как HMAC SHA256 или RSA. Заголовок представлен в формате JSON, а затем кодируется в строку с использованием Base64Url. Например:

{
  "alg": "HS256",
  "typ": "JWT"
}

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

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9

2. Полезная нагрузка

Полезная нагрузка состоит из ряда утверждений, содержащих информацию об сущности (обычно пользователя) и другие данные. Полезная нагрузка может включать несколько предопределенных утверждений (также известных как Зарегистрированные утверждения) и пользовательские утверждения (Личные утверждения).

Предопределенные утверждения могут включать:

  • iss (Issuer): Издатель
  • exp (Expiration Time): Время истечения
  • sub (Subject): Тема
  • aud (Audience): Аудитория
  • iat (Issued At): Выдано
  • nbf (Not Before): Начало действия
  • jti (JWT ID): Уникальный идентификатор JWT

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

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true,
  "iat": 1516239022
}

Эта информация также будет закодирована с использованием Base64Url, и вы можете получить строку, подобную этой:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0

3. Подпись

Раздел подписи используется для подписи двух закодированных строк, упомянутых выше, с целью проверки того, что сообщение не было изменено во время передачи. Сначала вам нужно указать ключ (если используется алгоритм HMAC SHA256), а затем использовать алгоритм, указанный в заголовке, чтобы подписать заголовок и полезную нагрузку.

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

HeaderEncoded.HeaderPayload

Псевдокод для подписи их с использованием ключа может быть следующим:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secretKey)

Полученная подписанная строка может выглядеть так:

dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

Полный JWT

Комбинирование этих трех частей с точкой (.) в качестве разделителя формирует полный JWT:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.dBjftJeZ4CVPmTaoyL4IiArYfL4kH0jOspm6XwbcJXY

Стандарт JWT является очень гибким и может хранить любую необходимую информацию в полезной нагрузке (но в целях уменьшения размера JWT и в целях безопасности, конфиденциальную информацию стоит не хранить), а также гарантирует целостность этой информации через подпись.

Установка библиотеки JWT для Go

В сообществе Go доступен пакет github.com/golang-jwt/jwt для работы с JWT. Установка этой библиотеки очень проста, просто выполните следующую команду в каталоге вашего проекта:

go get -u github.com/golang-jwt/jwt/v5

После установки вы можете включить ее в свой import следующим образом:

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

Создание простого токена

Чтобы создать простой токен JWT на Go с использованием алгоритма HS256, выполните следующие шаги:

Сначала создайте новый объект Token с использованием алгоритма HS256:

token := jwt.New(jwt.SigningMethodHS256)

Затем используйте метод SignedString для генерации строкового представления этого токена, передав ключ, который вы будете использовать для подписи.

var mySigningKey = []byte("ваш-секрет-256-бит")
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("Произошла ошибка: %v", err)
}
fmt.Println(strToken)

Это сгенерирует простой токен без каких-либо утверждений.

Создание токена с параметрами

Одной из основных функций JWT является передача информации. Эта информация кодируется в токене через утверждения. Например, давайте создадим пользовательские утверждения:

// Структура пользовательских утверждений
type MyClaims struct {
	jwt.RegisteredClaims
	Username string `json:"username"`
	Admin    bool   `json:"admin"`
}

// Создание токена с пользовательскими утверждениями
claims := MyClaims{
    RegisteredClaims: jwt.RegisteredClaims{},
    Username: "мое_имя_пользователя",
	Admin: true,
}

token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)

// Секретный ключ для подписи
var mySigningKey = []byte("ваш-секрет-256-бит")

// Генерация токена в строковом формате
strToken, err := token.SignedString(mySigningKey)
if err != nil {
    log.Fatalf("Произошла ошибка: %v", err)
}

fmt.Println(strToken)

В вышеприведенном коде мы определили структуру MyClaims для содержания зарегистрированных утверждений, а также некоторой пользовательской информации. Затем мы все еще используем SignedString для генерации строки токена.

Разбор и валидация токена

Разбор и проверка JWT очень важны, и вы можете сделать это следующим образом:

// Мы будем использовать ту же структуру MyClaims
tokenString := "ваша-JWT-строка"

// Нам нужно определить функцию, которую пакет jwt будет использовать для разбора tokenString
keyFunc := func(t *jwt.Token) (interface{}, error) {
	// Проверяем, что используется ожидаемый метод подписи
	if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
	    return nil, fmt.Errorf("Неожиданный метод подписи: %v", t.Header["alg"])
	}
	// Возвращаем секретный ключ для jwt токена, в формате []byte, совпадающий с ключом, использованным для подписи ранее
	return mySigningKey, nil
}

// Разбор токена
claims := &MyClaims{}
parsedToken, err := jwt.ParseWithClaims(tokenString, claims, keyFunc)
if err != nil {
	log.Fatalf("Ошибка разбора: %v", err)
}

if !parsedToken.Valid {
    log.Fatalf("Недействительный токен")
}

// На этом этапе parsedToken был проверен, и мы можем прочитать утверждения
fmt.Printf("Пользователь: %s, Администратор: %v\n", claims.Username, claims.Admin)

В вышеприведенном коде мы предоставили строку токена, экземпляр MyClaims и функцию ключа keyFunc функции jwt.ParseWithClaims. Эта функция будет проверять подпись и разбирать токен, заполняя переменную claims, если токен действителен.