Безопасная аутентификация в Go: пишем код без уязвимостей

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Почему аутентификация в Go — это не шутки?

Go — язык, который выбирают за скорость и надёжность. Но даже самый быстрый сервер превращается в тыкву, если через него можно украсть пароли. Безопасная аутентификация в Go приложениях — это не просто "поставил библиотеку и забыл". Это целая наука: как хранить пароли, как не провалить JWT, как защититься от CSRF, когда всё уже горит.

В этой статье мы не будем читать лекции про теорию. Мы напишем код. Реальный, рабочий, который можно сразу вставить в свой проект. Разберём три кита безопасности: хеширование паролей (bcrypt), работу с JWT-токенами и защиту от межсайтовой подделки запросов (CSRF).

Хешируем пароли правильно: почему md5 — зло

Первый шаг к безопасности — никогда, слышите, никогда не хранить пароли в открытом виде. Даже если вы доверяете своей базе данных. Взлом может случиться с кем угодно (спросите Yahoo или Adobe). Правильное хеширование — ваш щит.

В Go стандартный выбор — golang.org/x/crypto/bcrypt. Почему именно bcrypt? Он устойчив к атакам перебором (включает "соль" и замедляет вычисления). MD5 или SHA-1 считаются за микросекунды. Bcrypt — за сотни миллисекунд. Для злоумышленника это катастрофа, для пользователя — незаметно.

Пример: регистрация пользователя

package main

import (
    "fmt"
    "golang.org/x/crypto/bcrypt"
)

func hashPassword(password string) (string, error) {
    bytes, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
    return string(bytes), err
}

func checkPasswordHash(password, hash string) bool {
    err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
    return err == nil
}

func main() {
    password := "mySecretPass123"
    hash, _ := hashPassword(password)

    fmt.Println("Пароль:", password)
    fmt.Println("Хеш:  ", hash)

    match := checkPasswordHash(password, hash)
    fmt.Println("Совпадает:", match)
}

Обратите внимание: bcrypt.DefaultCost — это 10 итераций. Для production можно увеличить до 12-14, но не перебарщивайте, иначе сервер ляжет под нагрузкой. Тестируйте на своём железе.

Проверка пароля при входе

Всё просто: берём пароль из формы, берём хеш из базы, сравниваем через bcrypt.CompareHashAndPassword. Если вернул nil — пользователь свой. Если ошибка — шлём 401.

func loginHandler(w http.ResponseWriter, r *http.Request) {
    // получаем password из формы
    storedHash := getUserHashFromDB(r.FormValue("email"))
    if !checkPasswordHash(r.FormValue("password"), storedHash) {
        http.Error(w, "Неверный пароль", http.StatusUnauthorized)
        return
    }
    // создаём сессию или токен
}

JWT: лёгкая аутентификация с подвохом

После того как пользователь ввёл пароль, ему нужно выдать "пропуск". Один из популярных способов — JWT (JSON Web Token). Это компактный токен, который содержит данные пользователя и подпись. Но дьявол в деталях: неправильная реализация JWT может сделать ваше приложение уязвимым.

Правильная генерация JWT в Go

Используем библиотеку github.com/golang-jwt/jwt/v5. Никогда не пишите свой велосипед для криптографии — только проверенные библиотеки.

import (
    "time"
    "github.com/golang-jwt/jwt/v5"
)

var jwtKey = []byte("my_secret_key_which_must_be_long_and_random")

type Claims struct {
    Username string `json:"username"`
    jwt.RegisteredClaims
}

func generateToken(username string) (string, error) {
    expirationTime := time.Now().Add(15 * time.Minute)
    claims := &Claims{
        Username: username,
        RegisteredClaims: jwt.RegisteredClaims{
            ExpiresAt: jwt.NewNumericDate(expirationTime),
        },
    }

    token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
    return token.SignedString(jwtKey)
}

Важно! Секретный ключ (jwtKey) должен быть длинным, случайным и храниться в переменных окружения, а не в коде. Никогда не коммитьте его в Git.

Валидация JWT (middleware)

Теперь каждый защищённый запрос должен проверять токен. Создадим middleware для обработчика:

func authMiddleware(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        tokenStr := r.Header.Get("Authorization")
        if tokenStr == "" {
            http.Error(w, "Токен отсутствует", http.StatusUnauthorized)
            return
        }

        // Убираем "Bearer "
        if len(tokenStr) > 7 && tokenStr[:7] == "Bearer " {
            tokenStr = tokenStr[7:]
        }

        claims := &Claims{}
        token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
            return jwtKey, nil
        })

        if err != nil || !token.Valid {
            http.Error(w, "Неверный токен", http.StatusUnauthorized)
            return
        }

        // Передаём имя пользователя в контекст
        ctx := context.WithValue(r.Context(), "username", claims.Username)
        next.ServeHTTP(w, r.WithContext(ctx))
    })
}

Используйте этот middleware для всех маршрутов, которые требуют аутентификации. Например:

mux := http.NewServeMux()
mux.Handle("/profile", authMiddleware(http.HandlerFunc(profileHandler)))

CSRF: невидимый враг

Даже если вы идеально настроили JWT, ваш сайт может быть атакован через CSRF (Cross-Site Request Forgery). Суть: пользователь заходит на вредоносный сайт, а тот отправляет запрос от его имени на ваш сервер. Если у пользователя есть активная сессия или токен в куках — злоумышленник может сменить пароль или перевести деньги.

Как защититься? Три рабочих метода:

  • SameSite Strict для кук (но это не панацея)
  • CSRF-токены — уникальный ключ, который сервер вставляет в форму и проверяет при отправке
  • Проверка Origin/Referer заголовков

В Go проще всего использовать готовую библиотеку github.com/gorilla/csrf. Она автоматически добавляет CSRF-токен в формы и проверяет его.

Пример с gorilla/csrf

import (
    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
)

func main() {
    r := mux.NewRouter()
    r.HandleFunc("/form", showForm)
    r.HandleFunc("/submit", submitForm)

    // Защищаем все маршруты
    csrfMiddleware := csrf.Protect(
        []byte("32-byte-long-auth-key-here!"),
        csrf.Secure(false), // true, если HTTPS
    )                

Блоги

Рекомендаций по книгам