jsonwebtoken (npm) → github.com/golang-jwt/jwt/v5
bcrypt (npm) → golang.org/x/crypto/bcrypt
cors (npm) → ручная настройка заголовков
express-rate-limit → golang.org/x/time/rate или свой middleware
helmet → ручные security headers
mkdir go-security && cd go-security
go mod init go-security
# Устанавливаем зависимости
go get github.com/golang-jwt/jwt/v5
go get golang.org/x/crypto/bcrypt
go get golang.org/x/time/rate
go get github.com/google/uuid
go mod tidy
# Структура
mkdir -p internal/auth
mkdir -p internal/middleware
mkdir -p cmd/server
internal/auth/jwt.gopackage auth
import (
"fmt"
"time"
"github.com/golang-jwt/jwt/v5"
"github.com/google/uuid"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ JWT СЕРВИС ║
// ╚══════════════════════════════════════════════════════════╝
// JWTService — сервис для работы с JWT
type JWTService struct {
secret []byte
accessTTL time.Duration
refreshTTL time.Duration
}
// Claims — пользовательские claims в JWT
type Claims struct {
jwt.RegisteredClaims
UserID string `json:"user_id"`
Email string `json:"email"`
Role string `json:"role"`
}
// TokenPair — пара токенов
type TokenPair struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
ExpiresIn int64 `json:"expires_in"` // секунды
}
// NewJWTService — конструктор
func NewJWTService(secret string, accessTTL, refreshTTL time.Duration) *JWTService {
return &JWTService{
secret: []byte(secret),
accessTTL: accessTTL,
refreshTTL: refreshTTL,
}
}
// GenerateTokenPair — создаёт access + refresh токены
func (s *JWTService) GenerateTokenPair(userID, email, role string) (*TokenPair, error) {
now := time.Now()
// Access token
accessClaims := &Claims{
RegisteredClaims: jwt.RegisteredClaims{
ID: uuid.New().String(),
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(s.accessTTL)),
Issuer: "go-security",
},
UserID: userID,
Email: email,
Role: role,
}
accessToken, err := s.createToken(accessClaims)
if err != nil {
return nil, fmt.Errorf("create access token: %w", err)
}
// Refresh token (с другими claims)
refreshClaims := &Claims{
RegisteredClaims: jwt.RegisteredClaims{
ID: uuid.New().String(),
Subject: userID,
IssuedAt: jwt.NewNumericDate(now),
ExpiresAt: jwt.NewNumericDate(now.Add(s.refreshTTL)),
Issuer: "go-security",
},
UserID: userID,
}
refreshToken, err := s.createToken(refreshClaims)
if err != nil {
return nil, fmt.Errorf("create refresh token: %w", err)
}
return &TokenPair{
AccessToken: accessToken,
RefreshToken: refreshToken,
ExpiresIn: int64(s.accessTTL.Seconds()),
}, nil
}
// ValidateToken — проверяет токен и возвращает claims
func (s *JWTService) 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 s.secret, nil
},
)
if err != nil {
return nil, fmt.Errorf("parse token: %w", err)
}
claims, ok := token.Claims.(*Claims)
if !ok || !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}
// RefreshAccessToken — обновляет access token по refresh token
func (s *JWTService) RefreshAccessToken(refreshToken string) (*TokenPair, error) {
claims, err := s.ValidateToken(refreshToken)
if err != nil {
return nil, fmt.Errorf("invalid refresh token: %w", err)
}
// Генерируем новую пару (refresh token rotation)
return s.GenerateTokenPair(claims.UserID, claims.Email, claims.Role)
}
// createToken — создаёт подписанный токен
func (s *JWTService) createToken(claims *Claims) (string, error) {
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString(s.secret)
}
internal/auth/password.gopackage auth
import (
"errors"
"strings"
"unicode"
"golang.org/x/crypto/bcrypt"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ ХЕШИРОВАНИЕ ПАРОЛЕЙ ║
// ╚══════════════════════════════════════════════════════════╝
const (
BcryptCost = 12 // Стоимость bcrypt (4-31). 12 — хороший баланс
)
// HashPassword — хеширует пароль через bcrypt
func HashPassword(password string) (string, error) {
bytes, err := bcrypt.GenerateFromPassword([]byte(password), BcryptCost)
if err != nil {
return "", err
}
return string(bytes), nil
}
// CheckPassword — проверяет пароль с хешем
func CheckPassword(password, hash string) bool {
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
return err == nil
}
// ╔══════════════════════════════════════════════════════════╗
// ║ ВАЛИДАЦИЯ ПАРОЛЯ ║
// ╚══════════════════════════════════════════════════════════╝
var (
ErrPasswordTooShort = errors.New("password must be at least 8 characters")
ErrPasswordNoUpper = errors.New("password must contain at least one uppercase letter")
ErrPasswordNoLower = errors.New("password must contain at least one lowercase letter")
ErrPasswordNoDigit = errors.New("password must contain at least one digit")
ErrPasswordNoSpecial = errors.New("password must contain at least one special character")
ErrPasswordCommon = errors.New("password is too common")
)
// commonPasswords — список из 100 самых популярных паролей (сокращён)
var commonPasswords = map[string]bool{
"password": true, "12345678": true, "qwerty123": true,
"admin123": true, "letmein": true, "welcome1": true,
}
// ValidatePasswordStrength — проверяет пароль на соответствие требованиям
func ValidatePasswordStrength(password string) error {
if len(password) < 8 {
return ErrPasswordTooShort
}
var hasUpper, hasLower, hasDigit, hasSpecial bool
for _, ch := range password {
switch {
case unicode.IsUpper(ch):
hasUpper = true
case unicode.IsLower(ch):
hasLower = true
case unicode.IsDigit(ch):
hasDigit = true
case unicode.IsPunct(ch) || unicode.IsSymbol(ch):
hasSpecial = true
}
}
if !hasUpper { return ErrPasswordNoUpper }
if !hasLower { return ErrPasswordNoLower }
if !hasDigit { return ErrPasswordNoDigit }
if !hasSpecial { return ErrPasswordNoSpecial }
// Проверка на распространённые пароли
if commonPasswords[strings.ToLower(password)] {
return ErrPasswordCommon
}
return nil
}
internal/middleware/auth.gopackage middleware
import (
"context"
"net/http"
"strings"
"go-security/internal/auth"
)
// ContextKey — тип для ключей контекста
type ContextKey string
const (
UserClaimsKey ContextKey = "userClaims"
)
// AuthMiddleware — проверяет JWT в заголовке Authorization
func AuthMiddleware(jwtService *auth.JWTService) func(http.Handler) http.Handler {
return func(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, `{"error":"missing authorization header"}`, http.StatusUnauthorized)
return
}
// Ожидаем: "Bearer <token>"
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || !strings.EqualFold(parts[0], "bearer") {
http.Error(w, `{"error":"invalid authorization format"}`, http.StatusUnauthorized)
return
}
tokenString := parts[1]
// Валидируем токен
claims, err := jwtService.ValidateToken(tokenString)
if err != nil {
http.Error(w, `{"error":"invalid or expired token"}`, http.StatusUnauthorized)
return
}
// Кладём claims в контекст
ctx := context.WithValue(r.Context(), UserClaimsKey, claims)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
}
// RequireRole — middleware для проверки роли
func RequireRole(roles ...string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
claims, ok := r.Context().Value(UserClaimsKey).(*auth.Claims)
if !ok {
http.Error(w, `{"error":"unauthorized"}`, http.StatusUnauthorized)
return
}
for _, role := range roles {
if claims.Role == role {
next.ServeHTTP(w, r)
return
}
}
http.Error(w, `{"error":"forbidden"}`, http.StatusForbidden)
})
}
}
// GetClaimsFromContext — хелпер для извлечения claims
func GetClaimsFromContext(ctx context.Context) (*auth.Claims, bool) {
claims, ok := ctx.Value(UserClaimsKey).(*auth.Claims)
return claims, ok
}
internal/middleware/security.gopackage middleware
import (
"net/http"
"sync"
"time"
"golang.org/x/time/rate"
)
// ╔══════════════════════════════════════════════════════════╗
// ║ SECURITY HEADERS ║
// ╚══════════════════════════════════════════════════════════╝
// SecurityHeadersMiddleware — добавляет защитные заголовки
func SecurityHeadersMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
w.Header().Set("Permissions-Policy", "geolocation=(), microphone=()")
next.ServeHTTP(w, r)
})
}
// ╔══════════════════════════════════════════════════════════╗
// ║ CORS MIDDLEWARE ║
// ╚══════════════════════════════════════════════════════════╝
// CORSMiddleware — настраиваемый CORS
func CORSMiddleware(allowedOrigins []string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
allowed := false
for _, o := range allowedOrigins {
if o == "*" || o == origin {
allowed = true
break
}
}
if allowed {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Request-ID")
w.Header().Set("Access-Control-Allow-Credentials", "true")
w.Header().Set("Access-Control-Max-Age", "86400")
}
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}
// ╔══════════════════════════════════════════════════════════╗
// ║ RATE LIMITER (per-IP) ║
// ╚══════════════════════════════════════════════════════════╝
// IPRateLimiter — rate limiter по IP
type IPRateLimiter struct {
mu sync.Mutex
limiters map[string]*rate.Limiter
rate rate.Limit
burst int
}
// NewIPRateLimiter — создаёт rate limiter
func NewIPRateLimiter(r rate.Limit, burst int) *IPRateLimiter {
return &IPRateLimiter{
limiters: make(map[string]*rate.Limiter),
rate: r,
burst: burst,
}
}
// Allow — проверяет, разрешён ли запрос с этого IP
func (l *IPRateLimiter) Allow(ip string) bool {
l.mu.Lock()
limiter, exists := l.limiters[ip]
if !exists {
limiter = rate.NewLimiter(l.rate, l.burst)
l.limiters[ip] = limiter
}
l.mu.Unlock()
return limiter.Allow()
}
// Cleanup — удаляет старые записи (запускать по таймеру)
func (l *IPRateLimiter) Cleanup() {
l.mu.Lock()
defer l.mu.Unlock()
// В реальности — удалять записи старше N минут
l.limiters = make(map[string]*rate.Limiter)
}
// RateLimitMiddleware — middleware для rate limiting
func RateLimitMiddleware(limiter *IPRateLimiter) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ip := r.RemoteAddr
if !limiter.Allow(ip) {
w.Header().Set("Content-Type", "application/json")
w.Header().Set("Retry-After", "1")
w.WriteHeader(http.StatusTooManyRequests)
w.Write([]byte(`{"error":"rate limit exceeded"}`))
return
}
next.ServeHTTP(w, r)
})
}
}
cmd/server/main.gopackage main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"time"
"go-security/internal/auth"
"go-security/internal/middleware"
"golang.org/x/time/rate"
)
func main() {
log.SetFlags(log.LstdFlags | log.Lmicroseconds)
log.Println("🔒 Сервер безопасности...")
// Инициализация сервисов
jwtService := auth.NewJWTService(
"super-secret-key-change-me-in-production", // В реальности из env!
15*time.Minute, // Access token: 15 минут
7*24*time.Hour, // Refresh token: 7 дней
)
rateLimiter := middleware.NewIPRateLimiter(
rate.Limit(10), // 10 запросов в секунду
20, // Burst: до 20
)
mux := http.NewServeMux()
// ==========================================
// Публичные эндпоинты
// ==========================================
mux.HandleFunc("POST /api/login", func(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
json.NewDecoder(r.Body).Decode(&req)
// Имитация проверки пользователя (в реальности — БД)
if req.Email != "admin@example.com" {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
return
}
// Проверяем пароль (в реальности — сравнить с хешем из БД)
hash, _ := auth.HashPassword("securePass123!")
if !auth.CheckPassword(req.Password, hash) {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid credentials"})
return
}
// Генерируем токены
pair, err := jwtService.GenerateTokenPair("user-1", req.Email, "admin")
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "token generation failed"})
return
}
writeJSON(w, http.StatusOK, pair)
})
mux.HandleFunc("POST /api/register", func(w http.ResponseWriter, r *http.Request) {
var req struct {
Email string `json:"email"`
Password string `json:"password"`
}
json.NewDecoder(r.Body).Decode(&req)
// Валидация пароля
if err := auth.ValidatePasswordStrength(req.Password); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
// Хешируем пароль
hash, err := auth.HashPassword(req.Password)
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "hashing failed"})
return
}
log.Printf("Новый пользователь: %s (hash: %s...)", req.Email, hash[:20])
writeJSON(w, http.StatusCreated, map[string]string{"message": "user created"})
})
mux.HandleFunc("POST /api/refresh", func(w http.ResponseWriter, r *http.Request) {
var req struct {
RefreshToken string `json:"refresh_token"`
}
json.NewDecoder(r.Body).Decode(&req)
pair, err := jwtService.RefreshAccessToken(req.RefreshToken)
if err != nil {
writeJSON(w, http.StatusUnauthorized, map[string]string{"error": "invalid refresh token"})
return
}
writeJSON(w, http.StatusOK, pair)
})
// ==========================================
// Защищённые эндпоинты
// ==========================================
protectedMux := http.NewServeMux()
protectedMux.HandleFunc("GET /api/me", func(w http.ResponseWriter, r *http.Request) {
claims, _ := middleware.GetClaimsFromContext(r.Context())
writeJSON(w, http.StatusOK, map[string]any{
"user_id": claims.UserID,
"email": claims.Email,
"role": claims.Role,
})
})
// Admin-only эндпоинт
protectedMux.HandleFunc("GET /api/admin/users", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, []string{"user-1", "user-2", "user-3"})
})
// Применяем auth middleware
protectedHandler := middleware.AuthMiddleware(jwtService)(protectedMux)
// Admin routes — дополнительная проверка роли
adminHandler := middleware.RequireRole("admin")(protectedMux)
mux.Handle("/api/", protectedHandler)
mux.Handle("/api/admin/", adminHandler)
// ==========================================
// Глобальные middleware
// ==========================================
handler := middleware.SecurityHeadersMiddleware(mux)
handler = middleware.CORSMiddleware([]string{"http://localhost:3000"})(handler)
handler = middleware.RateLimitMiddleware(rateLimiter)(handler)
log.Println("✅ Сервер на :8080")
log.Println("Тестовые учётные данные:")
log.Println(" Email: admin@example.com")
log.Println(" Password: securePass123!")
http.ListenAndServe(":8080", handler)
}
func writeJSON(w http.ResponseWriter, status int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
# Запуск сервера
go run ./cmd/server/main.go
# 1. Регистрация (проверка валидации пароля)
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"Weak"}'
# Успешная регистрация
curl -X POST http://localhost:8080/api/register \
-H "Content-Type: application/json" \
-d '{"email":"user@example.com","password":"StrongPass123!"}'
# 2. Логин
curl -X POST http://localhost:8080/api/login \
-H "Content-Type: application/json" \
-d '{"email":"admin@example.com","password":"securePass123!"}'
# Сохраняем токен
TOKEN="eyJ..." # Подставьте полученный access_token
# 3. Доступ к защищённому эндпоинту
curl http://localhost:8080/api/me \
-H "Authorization: Bearer $TOKEN"
# 4. Проверка rate limiter (отправить 25 запросов)
for i in {1..25}; do
curl -s http://localhost:8080/api/me \
-H "Authorization: Bearer $TOKEN" &
done
| Уровень | Инструмент | Защита от |
|---|---|---|
| Аутентификация | JWT (access + refresh) | Несанкционированный доступ |
| Авторизация | Middleware RequireRole | Доступ к чужим ресурсам |
| Хеширование | bcrypt (cost=12) | Утечка паролей из БД |
| Валидация | ValidatePasswordStrength | Brute-force по слабым паролям |
| CORS | CORSMiddleware | Cross-origin атаки |
| Rate Limiting | IPRateLimiter | DDoS, brute-force |
| Headers | SecurityHeadersMiddleware | XSS, clickjacking, MIME-sniffing |
💡 Best practices от сеньоров:
💡 Для Node.js разработчика:
jsonwebtoken → golang-jwt/jwt. API очень похож: Sign, Parse, Claims.
bcrypt → golang.org/x/crypto/bcrypt. Те же методы: Hash, Compare.(req, res, next). В Go: func(http.Handler) http.Handler.express-rate-limit. В Go — golang.org/x/time/rate.