JWT (произносится как «джот»).JWT. На примере небольшого сервера покажем, как используется пакет jwt-go и как это всё работает в проектах на Go.JWT — HMAC. Это один из механизмов проверки целостности информации: он гарантирует, что данные, передаваемые или хранящиеся в ненадёжной среде, не были изменены посторонними. Другие популярные алгоритмы для подписи — RSA (от фамилий Rivest, Shamir и Adleman) и ECDSA (Elliptic Curve Digital Signature Algorithm).JWT нужен для того, чтобы передавать данные, которым может доверять сервер.JWT. После того как пользователь входит в систему, каждый последующий запрос включает JWT. Пользователь получает доступ к маршрутам, службам и ресурсам, разрёшенным с помощью этого токена. Единый вход — это функция, которая использует JWT.JWT — это способ безопасной передачи информации между сторонами. Так как JWT могут быть подписаны с использованием пар открытого и закрытого ключей, можно быть уверенным, что отправитель действительно является тем, за кого себя выдаёт.xxxxxxxxx.yyyyyyyyy.zzzzzzzzz{
"alg": "HS256",
"typ": "JWT"
} JWT.JWT утверждения:exp— срок годности утверждения токена;iat — дата выпуска токена в unix-переменной;iss — создатель заявления;aud — получатель токена.JWT. Это имя, почта, временная зона действия токена. Частные утверждения определяют создатели приложения. Компания может назначить специфический userId всем своим пользователям — и это попадёт в утверждения.{
"name": "Vlad",
"admin": true,
"nickname": "lekan"
} Base64Url для формирования второй части JWT.JWT.JWT представляет собой три строки Base64Url, разделённые точками. Их можно легко передавать по HTTP.JWT c закодированным заголовком и полезной нагрузкой, подписанный секретным ключом:JWT. Каждый раз, когда пользователь хочет получить доступ к защищённому ресурсу, пользовательский агент должен снова отправить JWT. Обычно это происходит в заголовке. Authorization: Bearer <token> JWT в заголовке Authorization. Если он присутствует, пользователю будет разрешён доступ к защищённым ресурсам.go mod init, чтобы создать файл go.mod для отслеживания зависимостей кода. Установите пакет jwt-go, выполнив команду:go get -u github.com/golang-jwt/jwt/v4 main.go. Весь код будем писать в пакете main — так будет проще разобраться в особенностях JWT на Go. JWT и одно пользовательское утверждение UserID. Это будет полезная нагрузка в сервисе. Создадим основную функцию main():package main
import (
"github.com/golang-jwt/jwt/v4"
)
// Claims — структура утверждений, которая включает стандартные утверждения и
// одно пользовательское UserID
type Claims struct {
jwt.RegisteredClaims
UserID int
}
func main() {
} jwt.RegisteredClaims представляет собой структуру, в которую входит набор утверждений JWT c зарегистрированными именами утверждений из спецификации JWT. Выглядит она так:type RegisteredClaims struct {
// The `iss` (Issuer) claim. Смотрите в спецификации:
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1
Issuer string `json:"iss,omitempty"`
// The `sub` (Subject) claim. Смотрите в спецификации:
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.2
Subject string `json:"sub,omitempty"`
// The `aud` (Audience) claim. Смотрите в спецификации:
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.3
Audience ClaimStrings `json:"aud,omitempty"`
// The `exp` (Expiration Time) claim. Смотрите в спецификации:
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4
ExpiresAt *NumericDate `json:"exp,omitempty"`
// The `nbf` (Not Before) claim. Смотрите в спецификации:
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5
NotBefore *NumericDate `json:"nbf,omitempty"`
// The `iat` (Issued At) claim. Смотрите в спецификации:
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.6
IssuedAt *NumericDate `json:"iat,omitempty"`
// the `jti` (JWT ID) claim. Смотрите в спецификации:
// https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.7
ID string `json:"jti,omitempty"`
} JWT — ExpiresAt. Оно определит время жизни токена.main() вызовите эту функцию и выведите на экран токен. package main
import (
"github.com/golang-jwt/jwt/v4"
)
// Claims — структура утверждений, которая включает стандартные утверждения
// и одно пользовательское — UserID
type Claims struct {
jwt.RegisteredClaims
UserID int
}
const TOKEN_EXP = time.Hour * 3
const SECRET_KEY = "supersecretkey"
func main() {
tokenString, err := BuildJWTString()
if err != nil {
log.Fatal(err)
}
fmt.Println(tokenString)
}
// BuildJWTString создаёт токен и возвращает его в виде строки.
func BuildJWTString() (string, error) {
// создаём новый токен с алгоритмом подписи HS256 и утверждениями — Claims
token := jwt.NewWithClaims(jwt.SigningMethodHS256, Claims {
RegisteredClaims: jwt.RegisteredClaims{
// когда создан токен
ExpiresAt: jwt.NewNumericDate(time.Now().Add(TOKEN_EXP)),
},
// собственное утверждение
UserID: 1,
})
// создаём строку токена
tokenString, err := token.SignedString([]byte(SECRET_KEY))
if err != nil {
return "", err
}
// возвращаем строку токена
return tokenString, nil
} ExpiresAt — использовали функцию jwt.NewNumericDate(t time.Time) *NumericDate. Она принимает стандартную библиотечную структуру time.Time и возвращает указатель на структуру NumericDate, которая выглядит так:type NumericDate struct {
time.Time
} JWT.jwt.NewWithClaims(method SigningMethod, claims Claims) *Token создаёт новый токен с указанными методом подписи и утверждениями. В примере использовали метод подписи HS256, передав jwt.SigningMethodHS256.Claims, которую объявили. В неё входят зарегистрированное утверждение ExpiresAt и собственное утверждение UserID.UserID. Для этого напишем ещё одну функцию, которая будет принимать строку токена и возвращать ID пользователя.func GetUserID(tokenString string) int {
// создаём экземпляр структуры с утверждениями
claims := &Claims{}
// парсим из строки токена tokenString в структуру claims
jwt.ParseWithClaims(tokenString, claims, func(t *jwt.Token) (interface{}, error) {
return []byte(SECRET_KEY), nil
})
// возвращаем ID пользователя в читаемом виде
return claims.UserID
} main() строку для вывода UserID с вызовом новой функции — fmt.Println(GetUserID(tokenString)).Claims, куда можно сохранить всё, что находится в токене. Затем парсим строку токена с помощью функции ParseWithClaims(tokenString string, claims Claims, keyFunc KeyFunc, options ...ParserOption) (*Token, error).tokenString и claims?tokenString — это строка токена, claims — утверждения, которые объявили ранее.keyFunc() — это функция обратного вызова, которая используется методами Parse()?func(*Token) (interface{}, error). Эта функция предоставляет секретный ключ для проверки токена. Она принимает проанализированный, но не проверенный токен. На основании его заголовка функция определяет, какой секретный ключ использовать для проверки, и возвращает секретный ключ или ошибку.UserID из структуры Claims.UserID, равный 1:go run main.go
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEwODAwMDAwMDAwMDAwLCJVc2VySUQiOjF9.C1R1RcpPmnSFraHb_PfnLuVUl1UxeMmjGJIJ8NsHb3E
1 UserID в структуре Claims в функции BuildJWTString() на другое число и снова запустим программу:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEwODAwMDAwMDAwMDAwLCJVc2VySUQiOjE3fQ.4btPNlxdXO25pbkuTmnb_B0jRV-7_rGUCMCxK9HwTS4
17 GetUserId(), чтобы выполнить проверку валидности токена:func GetUserId(tokenString string) int {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims,
func(t *jwt.Token) (interface{}, error) {
return []byte(SECRET_KEY), nil
})
if err != nil {
return -1
}
if !token.Valid {
fmt.Println("Token is not valid")
return -1
}
fmt.Println("Token is valid")
return claims.UserID
} token и err. Если при проверке возникнет ошибка, вернётся -1. Если токен окажется невалидным, тоже вернётся значение -1. Если ошибок нет и токен прошёл проверку, вернётся ID пользователя и выведется сообщение, что токен валидный.go run main.go
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjEwODAwMDAwMDAwMDAwLCJVc2VySUQiOjE3fQ.4btPNlxdXO25pbkuTmnb_B0jRV-7_rGUCMCxK9HwTS4
Token is valid
17 JWT — это стандарт, который даёт большую гибкость в выборе алгоритмов подписи. Он прост в использовании и предоставляет сервисы без сохранения состояния. Но у любой медали — две стороны. При работе с JWT можно столкнуться с рядом проблем. Рассмотрим некоторые из них. JWT предоставляет обширный набор алгоритмов, включая те, в которых уже известна уязвимость. Например RSA c PKCSv1.5, ECDSA. Разработчикам без достаточного опыта в области безопасности будет сложно разобраться, какой алгоритм лучше всего использовать.JWT включает алгоритм подписи в заголовок токена, а значит, злоумышленник может просто установить заголовок alg в none и обойти процесс проверки подписи. Эта уязвимость была обнаружена и исправлена во многих библиотеках. Но всё же рекомендуем проверять подобные нюансы.GetUserID():func GetUserId(tokenString string) int {
claims := &Claims{}
token, err := jwt.ParseWithClaims(tokenString, claims,
func(t *jwt.Token) (interface{}, error) {
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", t.Header["alg"])
}
return []byte(SECRET_KEY), nil
})
if err != nil {
return -1
}
if !token.Valid {
fmt.Println("Token is not valid")
return -1
}
fmt.Println("Token os valid")
return claims.UserID JWT, и как избежать этих проблем. Authorization Bearer ${access_token}.localStorage.HttpOnly и защищённые куки-файлы, то из JavaScript нельзя получить доступ к этим файлам. Даже если атакующий сможет запустить свой код на вашей странице, ему не удастся прочитать токен доступа из куки-файла.JWT. Размер куки-файлов ограничен 4 Кб.Authorization. В таком случае вы не сможете хранить токены в куки-файлах.jwt-go.JWT.