Введение: зачем нужны OAuth 2.0 и JWT в Go?
Современные веб-приложения и API редко обходятся без системы авторизации. Если вы пишете на Go (Golang) и хотите защитить свои эндпоинты, вам нужно разобраться с двумя ключевыми технологиями: OAuth 2.0 (протокол делегирования доступа) и JWT (JSON Web Token — компактный формат токена).
В этой статье мы разберём, как реализовать безопасную авторизацию на Go с нуля. Вы узнаете:
- Чем отличается OAuth 2.0 от JWT и как они работают вместе.
- Как генерировать и проверять JWT-токены на Go.
- Как построить минимальную OAuth 2.0-авторизацию (Client Credentials flow).
- Как защитить REST API с помощью middleware.
Статья рассчитана на начинающих разработчиков, которые уже знакомы с основами Go и HTTP-серверами.
Основы: OAuth 2.0 и JWT — не одно и то же
Многие новички путают эти понятия. Давайте сразу расставим точки над i.
Что такое OAuth 2.0?
OAuth 2.0 — это протокол авторизации, который позволяет одному сервису (клиенту) получить ограниченный доступ к ресурсам другого сервиса (сервера) от имени пользователя. Представьте, что вы хотите войти на сайт через Google. Сайт не получает ваш пароль — он получает от Google временный токен, который даёт доступ только к вашей почте (и то с вашего согласия).
Основные роли в OAuth 2.0:
- Resource Owner — владелец данных (обычно пользователь).
- Client — приложение, которое хочет получить доступ.
- Authorization Server — сервер, который выдаёт токены после проверки.
- Resource Server — сервер, который хранит защищённые данные и проверяет токены.
Что такое JWT?
JWT (JSON Web Token) — это формат самого токена. Он представляет собой строку из трёх частей, разделённых точками: header.payload.signature. JWT может содержать любые данные (например, ID пользователя, роль, срок действия) и подписан секретным ключом, чтобы его нельзя было подделать.
OAuth 2.0 не привязан к JWT — токен может быть любой строкой. Однако на практике JWT используется чаще всего, потому что он самодостаточен: серверу не нужно хранить сессии в базе данных, всю информацию можно проверить на лету.
Реализация JWT на Go: генерация и проверка токенов
Начнём с практики. Для работы с JWT в Go мы будем использовать библиотеку golang-jwt/jwt/v5. Установите её:
go get github.com/golang-jwt/jwt/v5
Генерация Access Token
Создадим функцию, которая принимает ID пользователя и возвращает подписанный JWT-токен с временем жизни 1 час.
package main
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
var jwtSecret = []byte("your-secret-key") // В продакшене храните в .env!
type Claims struct {
UserID uint `json:"user_id"`
jwt.RegisteredClaims
}
func GenerateAccessToken(userID uint) (string, error) {
claims := Claims{
UserID: userID,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(time.Now().Add(time.Hour)),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "my-app",
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(jwtSecret)
}
Проверка и парсинг JWT
Теперь напишем middleware, который будет проверять токен из заголовка Authorization: Bearer <token>.
func ValidateToken(tokenString string) (*Claims, error) {
token, err := jwt.ParseWithClaims(tokenString, &Claims{}, func(token *jwt.Token) (interface{}, error) {
// Проверяем, что используется правильный метод подписи
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtSecret, nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(*Claims); ok && token.Valid {
return claims, nil
}
return nil, fmt.Errorf("invalid token")
}
Middleware для защиты эндпоинтов
Оборачиваем любой эндпоинт в middleware, который извлекает пользователя из токена:
func AuthMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "Missing token", http.StatusUnauthorized)
return
}
// Ожидаем формат "Bearer <token>"
parts := strings.Split(authHeader, " ")
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "Invalid token format", http.StatusUnauthorized)
return
}
claims, err := ValidateToken(parts[1])
if err != nil {
http.Error(w, "Invalid or expired token", http.StatusUnauthorized)
return
}
// Сохраняем данные пользователя в контекст запроса
ctx := context.WithValue(r.Context(), "userID", claims.UserID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Реализация OAuth 2.0 на Go: Client Credentials Flow
Для простоты разберём самый распространённый сценарий для серверных приложений — Client Credentials Flow. В нём нет пользователя; два сервера обмениваются токенами напрямую.
1. Эндпоинт для выдачи токена
Создаём обработчик /oauth/token, который проверяет client_id и client_secret и возвращает JWT.
func TokenHandler(w http.ResponseWriter, r *http.Request) {
// В реальном проекте client_id/secret проверяются из БД
clientID := r.PostFormValue("client_id")
clientSecret := r.PostFormValue("client_secret")
if clientID != "my-client" || clientSecret != "my-secret" {
http.Error(w, `{"error":"invalid_client"}`, http.StatusUnauthorized