Почему аутентификация в 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
)