On branch main
modified: internal/domain/auth/dto.go modified: internal/domain/auth/handler.go modified: internal/domain/auth/router.go modified: internal/domain/auth/servcie.go modified: internal/middleware/auth.go modified: internal/router/router.go auth implemented without reset password
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
// dto.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
@@ -21,10 +22,16 @@ type LoginRequest struct {
|
||||
|
||||
// AuthResponse структура ответа при успешной аутентификации
|
||||
type AuthResponse struct {
|
||||
Token string `json:"token"`
|
||||
RefreshToken string `json:"refresh_token,omitempty"`
|
||||
ExpiresAt time.Time `json:"expires_at"`
|
||||
User UserInfo `json:"user"`
|
||||
Token string `json:"token"` // Access token для Bearer авторизации
|
||||
ExpiresAt time.Time `json:"expires_at"` // Время истечения access token
|
||||
User UserInfo `json:"user"`
|
||||
}
|
||||
|
||||
// RefreshTokenResponse структура ответа при обновлении токена
|
||||
type RefreshTokenResponse struct {
|
||||
Token string `json:"token"` // Новый access token
|
||||
ExpiresAt time.Time `json:"expires_at"` // Время истечения нового access token
|
||||
User UserInfo `json:"user"`
|
||||
}
|
||||
|
||||
// ResetPasswordRequest - запрос на сброс пароля
|
||||
@@ -32,7 +39,7 @@ type ResetPasswordRequest struct {
|
||||
Email string `json:"email" validate:"required,email"`
|
||||
}
|
||||
|
||||
// RefreshTokenRequest - запрос на обновление токена
|
||||
// RefreshTokenRequest - запрос на обновление токена (только для мобильных приложений)
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" validate:"required"`
|
||||
}
|
||||
@@ -57,4 +64,6 @@ var (
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrInvalidPassword = errors.New("invalid password")
|
||||
ErrUserAlreadyExists = errors.New("user with this email already exists")
|
||||
)
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrTokenExpired = errors.New("token expired")
|
||||
)
|
||||
@@ -1,3 +1,4 @@
|
||||
// handler.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
@@ -5,6 +6,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"api_yal/internal/logger"
|
||||
"api_yal/internal/middleware"
|
||||
@@ -13,6 +15,12 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// Cookie константы
|
||||
const (
|
||||
RefreshTokenCookieName = "refresh_token"
|
||||
RefreshTokenExpiration = 7 * 24 * time.Hour // 7 дней
|
||||
)
|
||||
|
||||
// AuthHandler обработчик для аутентификации
|
||||
type AuthHandler struct {
|
||||
authService AuthService
|
||||
@@ -20,13 +28,39 @@ type AuthHandler struct {
|
||||
}
|
||||
|
||||
// NewAuthHandler создает новый экземпляр AuthHandler
|
||||
func NewAuthHandler(authService *AuthService) *AuthHandler {
|
||||
func NewAuthHandler(authService AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: *authService,
|
||||
authService: authService,
|
||||
validator: validator.New(),
|
||||
}
|
||||
}
|
||||
|
||||
// setRefreshTokenCookie устанавливает HttpOnly cookie с refresh token
|
||||
func (h *AuthHandler) setRefreshTokenCookie(w http.ResponseWriter, refreshToken string) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: RefreshTokenCookieName,
|
||||
Value: refreshToken,
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: true, // Всегда true в production
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: int(RefreshTokenExpiration.Seconds()),
|
||||
})
|
||||
}
|
||||
|
||||
// clearRefreshTokenCookie удаляет refresh token cookie
|
||||
func (h *AuthHandler) clearRefreshTokenCookie(w http.ResponseWriter) {
|
||||
http.SetCookie(w, &http.Cookie{
|
||||
Name: RefreshTokenCookieName,
|
||||
Value: "",
|
||||
Path: "/",
|
||||
HttpOnly: true,
|
||||
Secure: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
MaxAge: -1,
|
||||
})
|
||||
}
|
||||
|
||||
// Register регистрация аккаунта пользователя
|
||||
func (h *AuthHandler) Register(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
@@ -110,7 +144,7 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.authService.Login(req)
|
||||
response, refreshToken, err := h.authService.Login(req)
|
||||
if err != nil {
|
||||
l.Error("Ошибка входа: %v", zap.Error(err))
|
||||
|
||||
@@ -127,39 +161,81 @@ func (h *AuthHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Устанавливаем refresh token в HttpOnly cookie
|
||||
h.setRefreshTokenCookie(w, refreshToken)
|
||||
|
||||
l.Debug("Завершение обработки запроса входа")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// RefreshToken обновление токена
|
||||
// Поддерживает два способа получения refresh token:
|
||||
// 1. Из HttpOnly cookie (для web приложений)
|
||||
// 2. Из тела запроса (для мобильных приложений)
|
||||
func (h *AuthHandler) RefreshToken(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
l.Info("Начало обработки запроса обновления токена")
|
||||
|
||||
var req RefreshTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "Invalid request body", http.StatusBadRequest)
|
||||
var refreshToken string
|
||||
|
||||
// Пытаемся получить refresh token из cookie
|
||||
cookie, err := r.Cookie(RefreshTokenCookieName)
|
||||
if err == nil && cookie != nil {
|
||||
refreshToken = cookie.Value
|
||||
}
|
||||
|
||||
// Если в cookie нет, пробуем получить из тела запроса (для мобильных приложений)
|
||||
if refreshToken == "" {
|
||||
var req RefreshTokenRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err == nil {
|
||||
refreshToken = req.RefreshToken
|
||||
}
|
||||
}
|
||||
|
||||
if refreshToken == "" {
|
||||
http.Error(w, "Refresh token required", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.validator.Struct(req); err != nil {
|
||||
http.Error(w, "Invalid request", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
response, err := h.authService.RefreshToken(req.RefreshToken)
|
||||
response, err := h.authService.RefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
l.Error("Ошибка обновления токена", zap.Error(err))
|
||||
|
||||
if errors.Is(err, ErrInvalidToken) || errors.Is(err, ErrTokenExpired) {
|
||||
// Очищаем невалидный refresh token
|
||||
h.clearRefreshTokenCookie(w)
|
||||
http.Error(w, "Invalid or expired refresh token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Token refresh failed", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
// Генерируем новый refresh token
|
||||
newRefreshToken, err := h.generateNewRefreshTokenFromUser(response.User.ID)
|
||||
if err != nil {
|
||||
l.Error("Ошибка генерации нового refresh token", zap.Error(err))
|
||||
http.Error(w, "Failed to generate refresh token", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Обновляем refresh token в cookie
|
||||
h.setRefreshTokenCookie(w, newRefreshToken)
|
||||
|
||||
l.Info("Завершение обработки запроса обновления токена")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(response)
|
||||
}
|
||||
|
||||
// generateNewRefreshTokenFromUser генерирует новый refresh token для пользователя
|
||||
// Вспомогательная функция для обновления refresh token
|
||||
func (h *AuthHandler) generateNewRefreshTokenFromUser(userID uint) (string, error) {
|
||||
// Используем сервис для генерации refresh token
|
||||
return h.authService.GenerateNewRefreshToken(userID)
|
||||
}
|
||||
|
||||
// Logout выход пользователя
|
||||
func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
@@ -178,6 +254,9 @@ func (h *AuthHandler) Logout(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// Очищаем refresh token cookie
|
||||
h.clearRefreshTokenCookie(w)
|
||||
|
||||
l.Info("Завершение обработки запроса выхода")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
@@ -247,4 +326,4 @@ func (h *AuthHandler) ChangePassword(w http.ResponseWriter, r *http.Request) {
|
||||
"user_id": userID,
|
||||
"message": "Change password endpoint - to be implemented",
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,11 @@
|
||||
// router.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
"api_yal/internal/logger"
|
||||
"api_yal/internal/middleware"
|
||||
"api_yal/internal/repository"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"gorm.io/gorm"
|
||||
@@ -13,8 +15,16 @@ import (
|
||||
func RegisterRoutes(r chi.Router, db *gorm.DB, jwtSecret string) {
|
||||
// Создаем репозиторий и сервис
|
||||
accountRepo := repository.NewAccountRepository(db)
|
||||
authService := NewAuthService(accountRepo, jwtSecret)
|
||||
handler := NewAuthHandler(&authService)
|
||||
|
||||
// Конфигурация токенов
|
||||
authConfig := AuthServiceConfig{
|
||||
JWTSecret: jwtSecret,
|
||||
AccessTokenTTL: 15 * time.Minute, // Access token живет 15 минут
|
||||
RefreshTokenTTL: 7 * 24 * time.Hour, // Refresh token живет 7 дней
|
||||
}
|
||||
|
||||
authService := NewAuthService(accountRepo, authConfig)
|
||||
handler := NewAuthHandler(authService)
|
||||
|
||||
l := logger.Get()
|
||||
l.Debug("Регистрация маршрутов аутентификации")
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
// service.go
|
||||
package auth
|
||||
|
||||
import (
|
||||
@@ -16,25 +17,73 @@ import (
|
||||
// AuthService интерфейс сервиса аутентификации
|
||||
type AuthService interface {
|
||||
Register(req RegisterRequest) (*AuthResponse, error)
|
||||
Login(req LoginRequest) (*AuthResponse, error)
|
||||
RefreshToken(refreshToken string) (*AuthResponse, error)
|
||||
Login(req LoginRequest) (*AuthResponse, string, error) // Возвращает refresh token отдельно
|
||||
RefreshToken(refreshToken string) (*RefreshTokenResponse, error)
|
||||
Logout(userID uint) error
|
||||
ValidateAccessToken(tokenString string) (*jwt.MapClaims, error)
|
||||
GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error)
|
||||
GenerateNewRefreshToken(userID uint) (string, error)
|
||||
}
|
||||
|
||||
// authServiceImpl реализация сервиса аутентификации
|
||||
type authServiceImpl struct {
|
||||
accountRepo repository.AccountRepository
|
||||
jwtSecret []byte
|
||||
accountRepo repository.AccountRepository
|
||||
jwtSecret []byte
|
||||
accessTokenTTL time.Duration
|
||||
refreshTokenTTL time.Duration
|
||||
}
|
||||
|
||||
// AuthServiceConfig конфигурация для сервиса аутентификации
|
||||
type AuthServiceConfig struct {
|
||||
JWTSecret string
|
||||
AccessTokenTTL time.Duration // Рекомендуется 15-30 минут
|
||||
RefreshTokenTTL time.Duration // Рекомендуется 7-30 дней
|
||||
}
|
||||
|
||||
// NewAuthService создает новый экземпляр сервиса аутентификации
|
||||
func NewAuthService(accountRepo repository.AccountRepository, jwtSecret string) AuthService {
|
||||
func NewAuthService(accountRepo repository.AccountRepository, config AuthServiceConfig) AuthService {
|
||||
return &authServiceImpl{
|
||||
accountRepo: accountRepo,
|
||||
jwtSecret: []byte(jwtSecret),
|
||||
accountRepo: accountRepo,
|
||||
jwtSecret: []byte(config.JWTSecret),
|
||||
accessTokenTTL: config.AccessTokenTTL,
|
||||
refreshTokenTTL: config.RefreshTokenTTL,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateNewRefreshToken генерирует новый refresh token для пользователя по ID
|
||||
func (s *authServiceImpl) GenerateNewRefreshToken(userID uint) (string, error) {
|
||||
l := logger.Get()
|
||||
l.Info("Генерация нового refresh token", zap.Uint("userID", userID))
|
||||
|
||||
// Получаем пользователя из базы данных
|
||||
account, err := s.accountRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
l.Error("Пользователь не найден при генерации refresh token",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Error(err))
|
||||
return "", ErrUserNotFound
|
||||
}
|
||||
|
||||
// Проверяем, активен ли аккаунт
|
||||
if !account.IsActive {
|
||||
l.Error("Попытка генерации refresh token для деактивированного аккаунта",
|
||||
zap.Uint("userID", userID))
|
||||
return "", errors.New("account is deactivated")
|
||||
}
|
||||
|
||||
// Генерируем новый refresh token
|
||||
refreshToken, err := s.generateRefreshToken(account)
|
||||
if err != nil {
|
||||
l.Error("Ошибка генерации refresh token",
|
||||
zap.Uint("userID", userID),
|
||||
zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
l.Info("Refresh token успешно сгенерирован", zap.Uint("userID", userID))
|
||||
return refreshToken, nil
|
||||
}
|
||||
|
||||
// Register регистрирует нового пользователя
|
||||
func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
|
||||
l := logger.Get()
|
||||
@@ -74,17 +123,16 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Генерируем JWT токены
|
||||
accessToken, refreshToken, expiresAt, err := s.generateTokens(newAcc)
|
||||
// Генерируем access token
|
||||
accessToken, expiresAt, err := s.generateAccessToken(newAcc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Формируем ответ
|
||||
response := &AuthResponse{
|
||||
Token: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresAt: expiresAt,
|
||||
Token: accessToken,
|
||||
ExpiresAt: expiresAt,
|
||||
User: UserInfo{
|
||||
ID: newAcc.Base.ID,
|
||||
Email: newAcc.Email,
|
||||
@@ -99,8 +147,8 @@ func (s *authServiceImpl) Register(req RegisterRequest) (*AuthResponse, error) {
|
||||
return response, nil
|
||||
}
|
||||
|
||||
// Login аутентифицирует пользователя
|
||||
func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
|
||||
// Login аутентифицирует пользователя и возвращает access и refresh токены
|
||||
func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, string, error) {
|
||||
l := logger.Get()
|
||||
l.Info("Начало входа пользователя", zap.String("email", req.Email))
|
||||
|
||||
@@ -108,32 +156,36 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
|
||||
account, err := s.accountRepo.GetByEmail(req.Email)
|
||||
if err != nil {
|
||||
l.Error("Пользователь не найден", zap.String("email", req.Email), zap.Error(err))
|
||||
return nil, ErrUserNotFound
|
||||
return nil, "", ErrUserNotFound
|
||||
}
|
||||
|
||||
// Проверяем, активен ли аккаунт
|
||||
if !account.IsActive {
|
||||
l.Error("Аккаунт деактивирован", zap.String("email", req.Email))
|
||||
return nil, errors.New("account is deactivated")
|
||||
return nil, "", errors.New("account is deactivated")
|
||||
}
|
||||
|
||||
// Сравниваем пароли
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(account.PasswordHash), []byte(req.Password)); err != nil {
|
||||
l.Error("Неверный пароль для пользователя", zap.String("email", req.Email))
|
||||
return nil, ErrInvalidPassword
|
||||
return nil, "", ErrInvalidPassword
|
||||
}
|
||||
|
||||
// Генерируем JWT токены
|
||||
accessToken, refreshToken, expiresAt, err := s.generateTokens(account)
|
||||
// Генерируем токены
|
||||
accessToken, expiresAt, err := s.generateAccessToken(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
refreshToken, err := s.generateRefreshToken(account)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Формируем ответ
|
||||
response := &AuthResponse{
|
||||
Token: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
ExpiresAt: expiresAt,
|
||||
Token: accessToken,
|
||||
ExpiresAt: expiresAt,
|
||||
User: UserInfo{
|
||||
ID: account.Base.ID,
|
||||
Email: account.Email,
|
||||
@@ -145,53 +197,33 @@ func (s *authServiceImpl) Login(req LoginRequest) (*AuthResponse, error) {
|
||||
}
|
||||
|
||||
l.Info("Пользователь успешно вошел", zap.String("email", req.Email))
|
||||
return response, nil
|
||||
return response, refreshToken, nil
|
||||
}
|
||||
|
||||
// RefreshToken обновляет JWT токен по refresh token
|
||||
func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, error) {
|
||||
func (s *authServiceImpl) RefreshToken(refreshToken string) (*RefreshTokenResponse, error) {
|
||||
l := logger.Get()
|
||||
l.Info("Начало обновления токена")
|
||||
|
||||
// Парсим и валидируем refresh token
|
||||
token, err := jwt.Parse(refreshToken, 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 s.jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
l.Error("Невалидный refresh token", zap.Error(err))
|
||||
return nil, errors.New("invalid refresh token")
|
||||
}
|
||||
|
||||
// Получаем claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
l.Error("Не удалось получить claims из токена")
|
||||
return nil, errors.New("invalid token claims")
|
||||
}
|
||||
|
||||
// Проверяем тип токена (должен быть refresh)
|
||||
if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" {
|
||||
l.Error("Токен не является refresh токеном")
|
||||
return nil, errors.New("invalid token type")
|
||||
claims, err := s.validateRefreshToken(refreshToken)
|
||||
if err != nil {
|
||||
l.Error("Ошибка валидации refresh token", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Получаем ID пользователя из claims
|
||||
userIDStr, ok := claims["sub"].(string)
|
||||
userIDStr, ok := (*claims)["sub"].(string)
|
||||
if !ok {
|
||||
l.Error("Не удалось получить subject из токена")
|
||||
return nil, errors.New("invalid token claims")
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Конвертируем string в uint
|
||||
var userID uint
|
||||
if _, err := fmt.Sscan(userIDStr, &userID); err != nil {
|
||||
l.Error("Ошибка конвертации user ID", zap.Error(err))
|
||||
return nil, errors.New("invalid user id in token")
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Получаем пользователя из базы по ID
|
||||
@@ -207,18 +239,17 @@ func (s *authServiceImpl) RefreshToken(refreshToken string) (*AuthResponse, erro
|
||||
return nil, errors.New("account is deactivated")
|
||||
}
|
||||
|
||||
// Генерируем новую пару токенов
|
||||
accessToken, newRefreshToken, expiresAt, err := s.generateTokens(account)
|
||||
// Генерируем новый access token
|
||||
accessToken, expiresAt, err := s.generateAccessToken(account)
|
||||
if err != nil {
|
||||
l.Error("Ошибка генерации токенов", zap.Error(err))
|
||||
l.Error("Ошибка генерации access token", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Info("Обновление токена успешно завершено", zap.Uint("userID", userID))
|
||||
return &AuthResponse{
|
||||
Token: accessToken,
|
||||
RefreshToken: newRefreshToken,
|
||||
ExpiresAt: expiresAt,
|
||||
return &RefreshTokenResponse{
|
||||
Token: accessToken,
|
||||
ExpiresAt: expiresAt,
|
||||
User: UserInfo{
|
||||
ID: account.Base.ID,
|
||||
Email: account.Email,
|
||||
@@ -235,48 +266,132 @@ func (s *authServiceImpl) Logout(userID uint) error {
|
||||
l := logger.Get()
|
||||
l.Info("Выход пользователя", zap.Uint("userID", userID))
|
||||
// Здесь можно добавить логику инвалидации токенов (например, в Redis)
|
||||
// Для базовой реализации достаточно удалить cookie на клиенте
|
||||
l.Info("Выход успешно завершен", zap.Uint("userID", userID))
|
||||
return nil
|
||||
}
|
||||
|
||||
// generateTokens генерирует пару токенов: access и refresh
|
||||
func (s *authServiceImpl) generateTokens(account *models.Account) (accessToken, refreshToken string, expiresAt time.Time, err error) {
|
||||
// Access token expires in 15 minutes (более безопасно для access token)
|
||||
accessExpiresAt := time.Now().Add(15 * time.Minute)
|
||||
|
||||
// Refresh token expires in 7 days
|
||||
refreshExpiresAt := time.Now().Add(7 * 24 * time.Hour)
|
||||
// ValidateAccessToken валидирует access token
|
||||
func (s *authServiceImpl) ValidateAccessToken(tokenString string) (*jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, 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 s.jwtSecret, nil
|
||||
})
|
||||
|
||||
// Создаем access token
|
||||
accessClaims := jwt.MapClaims{
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Проверяем тип токена
|
||||
if tokenType, exists := claims["type"]; !exists || tokenType != "access" {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
|
||||
// GetUserFromToken получает пользователя из claims
|
||||
func (s *authServiceImpl) GetUserFromToken(claims *jwt.MapClaims) (*models.Account, error) {
|
||||
userIDStr, ok := (*claims)["sub"].(string)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
var userID uint
|
||||
if _, err := fmt.Sscan(userIDStr, &userID); err != nil {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
return s.accountRepo.GetByID(userID)
|
||||
}
|
||||
|
||||
// generateAccessToken генерирует access token
|
||||
func (s *authServiceImpl) generateAccessToken(account *models.Account) (string, time.Time, error) {
|
||||
expiresAt := time.Now().Add(s.accessTokenTTL)
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": fmt.Sprintf("%d", account.Base.ID),
|
||||
"email": account.Email,
|
||||
"role": account.Role,
|
||||
"exp": accessExpiresAt.Unix(),
|
||||
"exp": expiresAt.Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "access",
|
||||
}
|
||||
|
||||
accessTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, accessClaims)
|
||||
accessToken, err = accessTokenObj.SignedString(s.jwtSecret)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(s.jwtSecret)
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, err
|
||||
return "", time.Time{}, err
|
||||
}
|
||||
|
||||
// Создаем refresh token
|
||||
refreshClaims := jwt.MapClaims{
|
||||
return tokenString, expiresAt, nil
|
||||
}
|
||||
|
||||
// generateRefreshToken генерирует refresh token
|
||||
func (s *authServiceImpl) generateRefreshToken(account *models.Account) (string, error) {
|
||||
expiresAt := time.Now().Add(s.refreshTokenTTL)
|
||||
|
||||
claims := jwt.MapClaims{
|
||||
"sub": fmt.Sprintf("%d", account.Base.ID),
|
||||
"exp": refreshExpiresAt.Unix(),
|
||||
"exp": expiresAt.Unix(),
|
||||
"iat": time.Now().Unix(),
|
||||
"type": "refresh",
|
||||
"jti": fmt.Sprintf("refresh-%d-%d", account.Base.ID, time.Now().Unix()),
|
||||
}
|
||||
|
||||
refreshTokenObj := jwt.NewWithClaims(jwt.SigningMethodHS256, refreshClaims)
|
||||
refreshToken, err = refreshTokenObj.SignedString(s.jwtSecret)
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
tokenString, err := token.SignedString(s.jwtSecret)
|
||||
if err != nil {
|
||||
return "", "", time.Time{}, err
|
||||
return "", err
|
||||
}
|
||||
|
||||
return accessToken, refreshToken, accessExpiresAt, nil
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
// validateRefreshToken валидирует refresh token
|
||||
func (s *authServiceImpl) validateRefreshToken(tokenString string) (*jwt.MapClaims, error) {
|
||||
token, err := jwt.Parse(tokenString, 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 s.jwtSecret, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Проверяем тип токена
|
||||
if tokenType, exists := claims["type"]; !exists || tokenType != "refresh" {
|
||||
return nil, ErrInvalidToken
|
||||
}
|
||||
|
||||
// Проверяем время истечения
|
||||
exp, ok := claims["exp"].(float64)
|
||||
if ok && int64(exp) < time.Now().Unix() {
|
||||
return nil, ErrTokenExpired
|
||||
}
|
||||
|
||||
return &claims, nil
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// middleware/auth.go (обновленная версия с логированием)
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
@@ -14,12 +16,9 @@ import (
|
||||
type contextKey string
|
||||
|
||||
const (
|
||||
// UserIDKey ключ для хранения ID пользователя в контексте
|
||||
UserIDKey contextKey = "userID"
|
||||
// UserEmailKey ключ для хранения email пользователя в контексте
|
||||
UserIDKey contextKey = "userID"
|
||||
UserEmailKey contextKey = "userEmail"
|
||||
// UserRoleKey ключ для хранения роли пользователя в контексте
|
||||
UserRoleKey contextKey = "userRole"
|
||||
UserRoleKey contextKey = "userRole"
|
||||
)
|
||||
|
||||
// AuthMiddleware создает middleware для проверки JWT токена
|
||||
@@ -28,10 +27,15 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
l := logger.Get()
|
||||
|
||||
l.Info("=== AUTH MIDDLEWARE START ===")
|
||||
l.Info("Request path", zap.String("path", r.URL.Path))
|
||||
|
||||
// Получаем токен из заголовка Authorization
|
||||
authHeader := r.Header.Get("Authorization")
|
||||
l.Info("Authorization header", zap.String("header", authHeader))
|
||||
|
||||
if authHeader == "" {
|
||||
l.Debug("Отсутствует заголовок Authorization")
|
||||
l.Warn("Отсутствует заголовок Authorization")
|
||||
http.Error(w, "Authorization header required", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
@@ -39,63 +43,101 @@ func AuthMiddleware(jwtSecret string) func(http.Handler) http.Handler {
|
||||
// Ожидаем формат "Bearer <token>"
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
l.Debug("Неверный формат заголовка Authorization")
|
||||
l.Warn("Неверный формат заголовка Authorization",
|
||||
zap.Int("parts_count", len(parts)),
|
||||
zap.String("first_part", parts[0]))
|
||||
http.Error(w, "Invalid authorization header format", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
tokenString := parts[1]
|
||||
l.Info("Token extracted", zap.String("token_preview", tokenString[:min(20, len(tokenString))]+"..."))
|
||||
|
||||
// Парсим и валидируем токен
|
||||
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||
// Проверяем метод подписи
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
l.Error("Unexpected signing method",
|
||||
zap.String("method", token.Method.Alg()))
|
||||
return nil, jwt.ErrSignatureInvalid
|
||||
}
|
||||
return []byte(jwtSecret), nil
|
||||
})
|
||||
|
||||
if err != nil || !token.Valid {
|
||||
l.Debug("Невалидный токен: %v", zap.Error(err))
|
||||
if err != nil {
|
||||
l.Error("Token parse error", zap.Error(err))
|
||||
http.Error(w, "Invalid token: "+err.Error(), http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
if !token.Valid {
|
||||
l.Error("Token is not valid")
|
||||
http.Error(w, "Invalid token", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
l.Info("Token is valid")
|
||||
|
||||
// Извлекаем claims
|
||||
claims, ok := token.Claims.(jwt.MapClaims)
|
||||
if !ok {
|
||||
l.Debug("Не удалось извлечь claims из токена")
|
||||
l.Error("Failed to extract claims")
|
||||
http.Error(w, "Invalid token claims", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
|
||||
l.Info("Claims extracted", zap.Any("claims", claims))
|
||||
|
||||
// Проверяем тип токена (должен быть access)
|
||||
if tokenType, exists := claims["type"]; exists {
|
||||
l.Info("Token type", zap.String("type", tokenType.(string)))
|
||||
if tokenType != "access" {
|
||||
l.Error("Wrong token type, expected access", zap.String("type", tokenType.(string)))
|
||||
http.Error(w, "Invalid token type", http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Добавляем информацию о пользователе в контекст
|
||||
ctx := r.Context()
|
||||
|
||||
// Извлекаем userID из sub (subject)
|
||||
if userID, ok := claims["sub"].(float64); ok {
|
||||
ctx = context.WithValue(ctx, UserIDKey, uint(userID))
|
||||
// В claims sub хранится как string, а не float64
|
||||
if userID, ok := claims["sub"].(string); ok {
|
||||
l.Info("User ID from claims", zap.String("user_id_str", userID))
|
||||
// Конвертируем string в uint
|
||||
var userIDUint uint
|
||||
if _, err := fmt.Sscan(userID, &userIDUint); err == nil {
|
||||
ctx = context.WithValue(ctx, UserIDKey, userIDUint)
|
||||
l.Info("User ID added to context", zap.Uint("user_id", userIDUint))
|
||||
}
|
||||
} else {
|
||||
l.Error("sub claim not found or wrong type")
|
||||
}
|
||||
|
||||
// Извлекаем email
|
||||
if email, ok := claims["email"].(string); ok {
|
||||
ctx = context.WithValue(ctx, UserEmailKey, email)
|
||||
l.Info("Email added to context", zap.String("email", email))
|
||||
}
|
||||
|
||||
// Извлекаем роль
|
||||
if role, ok := claims["role"].(string); ok {
|
||||
ctx = context.WithValue(ctx, UserRoleKey, role)
|
||||
l.Info("Role added to context", zap.String("role", role))
|
||||
}
|
||||
|
||||
l.Info("=== AUTH MIDDLEWARE END ===")
|
||||
|
||||
// Передаем управление дальше с обновленным контекстом
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// AuthMiddlewareWithContext (для обратной совместимости)
|
||||
func AuthMiddlewareWithContext(next http.Handler) http.Handler {
|
||||
// Эта функция должна быть реализована в основном приложении
|
||||
// с передачей jwtSecret из конфигурации
|
||||
return next
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"api_yal/internal/config"
|
||||
"api_yal/internal/logger"
|
||||
"api_yal/internal/domain/auth"
|
||||
CastomMiddleware "api_yal/internal/middleware"
|
||||
"time"
|
||||
|
||||
"encoding/json"
|
||||
@@ -29,7 +28,6 @@ func SetupRouter(db *gorm.DB, config *config.Config) http.Handler {
|
||||
addProductionMiddleware(r, config)
|
||||
|
||||
// 2. Затем добавляем middleware аутентификации (он тоже применяется ко всем маршрутам)
|
||||
r.Use(CastomMiddleware.AuthMiddlewareWithContext)
|
||||
zapLogger.Debug("Auth middleware применён")
|
||||
|
||||
// 3. И только потом регистрируем маршруты
|
||||
|
||||
Reference in New Issue
Block a user