← Назад к вопросам
Как организовал работу по реализации авторизации?
2.3 Middle🔥 181 комментариев
#Soft Skills и рабочие процессы
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI3 апр. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Организация работы по реализации авторизации на фронтенде
Авторизация — критичная часть приложения, требующая надёжной архитектуры. Рассмотрю, как я организую эту работу в реальных проектах.
1. Архитектурное разделение — слои приложения
// frontend/
// ├── contexts/
// │ └── AuthContext.tsx // Источник истины для auth state
// ├── hooks/
// │ ├── useAuth.ts // Хук для доступа к auth
// │ └── useAuthGuard.ts // Защита маршрутов
// ├── lib/
// │ └── api.ts // HTTP клиент с автоматичным токеном
// ├── types/
// │ └── auth.ts // TypeScript типы
// ├── api/
// │ └── auth.ts // API слой (login, logout, refresh)
// └── middleware/
// └── auth.ts // Next.js middleware
// Слои зависимостей: presentation -> application -> domain
// Никакого API вызова в компонентах! Только через хуки и сервисы
2. Контекст как хранилище состояния авторизации
// contexts/AuthContext.tsx
import { createContext, useCallback, useEffect, useState } from "react";
interface AuthUser {
id: string;
email: string;
name: string;
}
interface AuthContextType {
user: AuthUser | null;
isLoading: boolean;
isAuthenticated: boolean;
login: (email: string, password: string) => Promise<void>;
logout: () => Promise<void>;
refreshToken: () => Promise<void>;
}
export const AuthContext = createContext<AuthContextType | null>(null);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<AuthUser | null>(null);
const [isLoading, setIsLoading] = useState(true);
// Восстановление сессии при загрузке приложения
useEffect(() => {
const restoreSession = async () => {
try {
const response = await fetch("/api/v1/auth/me", {
credentials: "include", // Отправляем cookies
});
if (response.ok) {
setUser(await response.json());
}
} catch (error) {
console.error("Failed to restore session:", error);
} finally {
setIsLoading(false);
}
};
restoreSession();
}, []);
const login = useCallback(async (email: string, password: string) => {
setIsLoading(true);
try {
const response = await fetch("/api/v1/auth/login", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ email, password }),
credentials: "include", // Сохраняем cookies
});
if (!response.ok) throw new Error("Login failed");
const userData = await response.json();
setUser(userData);
} finally {
setIsLoading(false);
}
}, []);
const logout = useCallback(async () => {
setIsLoading(true);
try {
await fetch("/api/v1/auth/logout", {
method: "POST",
credentials: "include",
});
setUser(null);
} finally {
setIsLoading(false);
}
}, []);
const refreshToken = useCallback(async () => {
try {
const response = await fetch("/api/v1/auth/refresh", {
method: "POST",
credentials: "include",
});
if (response.ok) {
setUser(await response.json());
}
} catch (error) {
setUser(null);
}
}, []);
return (
<AuthContext.Provider value={{ user, isLoading, isAuthenticated: !!user, login, logout, refreshToken }}>
{children}
</AuthContext.Provider>
);
}
3. Кастомный хук для удобного доступа к авторизации
// hooks/useAuth.ts
import { useContext } from "react";
import { AuthContext } from "@/contexts/AuthContext";
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within AuthProvider");
}
return context;
}
// Использование в компонентах
function UserProfile() {
const { user, isLoading, logout } = useAuth();
if (isLoading) return <div>Loading...</div>;
if (!user) return <div>Not authenticated</div>;
return (
<div>
<p>Welcome, {user.name}</p>
<button onClick={logout}>Logout</button>
</div>
);
}
4. HTTP клиент с автоматической обработкой токенов
// lib/api.ts
const API_BASE = process.env.NEXT_PUBLIC_API_URL;
const TOKEN_REFRESH_ENDPOINT = "/api/v1/auth/refresh";
let isRefreshing = false;
let failedQueue: Array<(token: string) => void> = [];
function processQueue(token: string) {
failedQueue.forEach(prom => prom(token));
failedQueue = [];
isRefreshing = false;
}
export async function fetchAPI(url: string, options: RequestInit = {}) {
const response = await fetch(`${API_BASE}${url}`, {
credentials: "include",
...options,
headers: {
"Content-Type": "application/json",
...options.headers,
},
});
// Обработка 401 — пытаемся обновить токен
if (response.status === 401) {
if (!isRefreshing) {
isRefreshing = true;
try {
const refreshResponse = await fetch(`${API_BASE}${TOKEN_REFRESH_ENDPOINT}`, {
method: "POST",
credentials: "include",
});
if (refreshResponse.ok) {
processQueue("");
// Повторяем исходный запрос
return fetchAPI(url, options);
}
} catch (error) {
failedQueue = [];
// Редирект на логин
window.location.href = "/login";
}
} else {
return new Promise(resolve => {
failedQueue.push(() => {
resolve(fetchAPI(url, options));
});
});
}
}
return response;
}
5. Защита маршрутов через middleware в Next.js
// middleware.ts
import { NextRequest, NextResponse } from "next/server";
export async function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const publicRoutes = ["/login", "/register", "/forgot-password"];
// Проверяем наличие сессии (через cookies)
const sessionCookie = request.cookies.get("session");
const isPublicRoute = publicRoutes.some(route => pathname.startsWith(route));
// Если есть токен и пытаемся попасть на логин — редирект на главную
if (sessionCookie && isPublicRoute) {
return NextResponse.redirect(new URL("/", request.url));
}
// Если нет токена и маршрут защищённый — редирект на логин
if (!sessionCookie && !isPublicRoute && pathname !== "/") {
return NextResponse.redirect(new URL("/login", request.url));
}
return NextResponse.next();
}
export const config = {
matcher: ["/((?!_next/static|_next/image|favicon.ico).*)"],
};
6. Организация работы — жизненный цикл разработки
// Этап 1: Планирование
// - Изучаю требования: какой тип auth (cookie, JWT, OAuth)?
// - Обсуждаю с бэком API контракты
// Этап 2: Подготовка
// 1. Создаю типы (auth.ts)
// 2. Создаю API слой (api/auth.ts)
// 3. Создаю контекст (contexts/AuthContext.tsx)
// 4. Создаю хуки (hooks/useAuth.ts)
// Этап 3: Страницы и компоненты
// 1. Страница логина (pages/login.tsx)
// 2. Страница регистрации (pages/register.tsx)
// 3. Protected компоненты (используют useAuth())
// Этап 4: Тестирование
// - Unit тесты для хуков
// - E2E тесты для полного флоу
// - Проверка refresh token логики
7. Обработка ошибок без дублирования
// Ошибки обрабатываются на границе (API слой) и не логируются в компонентах
// Правило: каждая ошибка логируется один раз
// api/auth.ts
export async function login(email: string, password: string) {
const response = await fetchAPI("/auth/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
if (!response.ok) {
const error = await response.json();
// Логируем ЗДЕСЬ и выбрасываем ошибку
console.error("Login failed:", error);
throw new Error(error.detail || "Login failed");
}
return response.json();
}
// Компонент просто ловит ошибку, не логирует
function LoginForm() {
const handleSubmit = async (e) => {
try {
await login(email, password);
// success
} catch (error) {
// Ошибка уже залогирована в API слое
// Компонент только показывает сообщение пользователю
setError(error.message);
}
};
}
Рекомендация
Ключевая организация:
- AuthContext — источник истины
- useAuth() — доступ из компонентов
- fetchAPI() — автоматическая обработка токенов
- API слой — вся бизнес-логика auth
- Middleware — защита маршрутов
- Один логирует — ошибка логируется в API слое, не в компонентах