Невозможно заставить маршрут перенаправлять на страницу входа в систему, если пользователь не аутентифицирован в приложении React/Django
Я использую React для фронтенда и Django для бэкенда, оба варианта являются новыми для меня, и я потратил кучу времени на эту проблему, не решив ее, поэтому я очень надеюсь, что кто-то знает ответ.
Когда я захожу на свой сайт http://localhost:4000/, не будучи авторизованным, я вижу черный экран с текстом "Loading..." (см. код protected_route ниже), вместо того чтобы быть перенаправленным на страницу входа в систему. Это происходит, когда я перехожу к любым защищенным маршрутам, не войдя в систему. В настоящее время единственными маршрутами, которые не защищены, являются страницы входа и регистрации, которые, если я вручную перехожу на них, работают правильно. После входа в систему токены JWT корректно сохраняются в куки httpOnly, но я вижу страницу "Loading...", пока не обновлю страницу вручную, тогда я вижу свои защищенные маршруты.
Я подумал, может быть, проверка подлинности происходит слишком быстро, и состояние еще не готово, и оно застревает на экране "Loading..."? Но я не знаю, как это исправить, если это так...
Я опубликую соответствующий код ниже, но я также поделюсь репозиторием github здесь.
Вот консоль браузера/сервера при переходе на страницу без авторизации:
GET http://localhost:8000/accounts/auth/user/ 401 (Unauthorized) AuthContext.tsx:37
GET http://localhost:8000/accounts/auth/user/ 401 (Unauthorized) AuthContext.tsx:37
POST http://localhost:8000/accounts/auth/token/refresh/ 401 (Unauthorized) axiosInstance.tsx:74
WARNING:django.request:Unauthorized: /accounts/auth/user/
[20/Jun/2024 01:47:58] "GET /accounts/auth/user/ HTTP/1.1" 401 58
Unauthorized: /accounts/auth/user/
WARNING:django.request:Unauthorized: /accounts/auth/user/
[20/Jun/2024 01:47:58] "GET /accounts/auth/user/ HTTP/1.1" 401 58
Unauthorized: /accounts/auth/token/refresh/
WARNING:django.request:Unauthorized: /accounts/auth/token/refresh/
[20/Jun/2024 01:47:58] "POST /accounts/auth/token/refresh/ HTTP/1.1" 401 67
AuthContext.tsx
import { createContext, useContext, useState, useEffect, ReactNode, FC } from 'react';
import axiosInstance from '../axiosInstance';
interface AuthContextType {
isAuthenticated: boolean;
loading: boolean;
login: (username: string, password: string, navigate: () => void) => Promise<void>;
logout: () => void;
register: (username: string, email: string, password: string, password2: string) => Promise<void>;
}
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export const AuthProvider: FC<{ children: ReactNode }> = ({ children }) => {
const [isAuthenticated, setIsAuthenticated] = useState<boolean>(false);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const checkAuthStatus = async () => {
try {
await axiosInstance.get('/auth/user/');
setIsAuthenticated(true);
} catch (error) {
if (error.response && error.response.status === 401) {
try {
await axiosInstance.post('/auth/token/refresh/');
await axiosInstance.get('/auth/user/');
setIsAuthenticated(true);
} catch (refreshError) {
setIsAuthenticated(false);
}
} else {
setIsAuthenticated(false);
}
} finally {
setLoading(false);
}
};
checkAuthStatus();
}, []);
// ... (login, logout, register functions)
return (
<AuthContext.Provider value={{ isAuthenticated, loading, login, logout, register }}>
{children}
</AuthContext.Provider>
);
};
export const useAuth = () => {
const context = useContext(AuthContext);
if (!context) {
throw new Error('useAuth must be used within an AuthProvider');
}
return context;
};
axiosInstance.tsx
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import Cookies from 'js-cookie'
const axiosInstance: AxiosInstance = axios.create({
baseURL: 'http://localhost:8000/accounts/',
timeout: 5000,
withCredentials: true,
})
axiosInstance.interceptors.request.use(
(config) => {
const csrfToken = Cookies.get('csrftoken')
if (csrfToken) {
config.headers['X-CSRFToken'] = csrfToken
}
return config
},
(error) => Promise.reject(error)
)
let isRefreshing = false
let failedQueue: any[] = []
const processQueue = (error: any, token: string | null = null) => {
failedQueue.forEach((prom: any) => {
if (error) {
prom.reject(error)
} else {
prom.resolve(token)
}
})
failedQueue = []
}
axiosInstance.interceptors.response.use(
(response: AxiosResponse) => response,
async (error) => {
const originalRequest: AxiosRequestConfig & { _retry?: boolean, _retryCount?: number } = error.config
if (error.response && error.response.status === 401 && !originalRequest._retry) {
if (isRefreshing) {
return new Promise((resolve, reject) => {
failedQueue.push({ resolve, reject })
})
.then((token: string) => {
originalRequest.headers['Authorization'] = 'Bearer ' + token
return axiosInstance(originalRequest)
})
.catch((err) => Promise.reject(err))
}
originalRequest._retry = true
originalRequest._retryCount = (originalRequest._retryCount || 0) + 1
if (originalRequest._retryCount > 3) {
return Promise.reject(error)
}
isRefreshing = true
return new Promise((resolve, reject) => {
axiosInstance.post('/auth/token/refresh/')
.then(({ data }) => {
axiosInstance.defaults.headers['Authorization'] = 'Bearer ' + data.access
originalRequest.headers['Authorization'] = 'Bearer ' + data.access
processQueue(null, data.access)
resolve(axiosInstance(originalRequest))
})
.catch((err) => {
processQueue(err, null)
reject(err)
})
.finally(() => {
isRefreshing = false
})
})
}
return Promise.reject(error)
}
)
export default axiosInstance
protected_route.tsx:
import { Navigate, Outlet } from 'react-router-dom'
import { useAuth } from '../contexts/AuthContext'
const ProtectedRoute = () => {
const { isAuthenticated, loading } = useAuth()
if (loading) {
return <div>Loading...</div> // Display a loading indicator while checking auth status
}
return isAuthenticated ? <Outlet /> : <Navigate to="/login" />
}
export default ProtectedRoute
router.tsx:
import { Routes, Route, BrowserRouter } from 'react-router-dom'
import ProtectedRoute from '../components/protected_route'
import Dashboard from '../pages/dashboard'
import Login from '../pages/login'
import Register from '../pages/register'
import TestPage from '../pages/testPage'
import MainLayout from '../layouts/main_layout'
import Trades from '../pages/trades'
import LoginSignUpLayout from '../layouts/login_register_layout'
const AppRoutes = () => (
<BrowserRouter>
<Routes>
<Route path="/" element={<LoginSignUpLayout />}>
<Route path="/login" element={<Login />} />
<Route path="/register" element={<Register />} />
</Route>
<Route element={<ProtectedRoute />}>
<Route element={<MainLayout />}>
<Route path="/" index element={<Dashboard />} />
<Route path="trades" element={<Trades />} />
<Route path="TestPage" element={<TestPage />} />
{/* Add more protected routes here */}
</Route>
</Route>
</Routes>
</BrowserRouter>
)
export default AppRoutes
Backend: cookie_jwt_middleware.py:
from django.utils.deprecation import MiddlewareMixin
class CookieJWTMiddleware(MiddlewareMixin):
def process_request(self, request):
access_token = request.COOKIES.get('access_token')
if access_token:
request.META['HTTP_AUTHORIZATION'] = f'Bearer {access_token}'
accounts/urls.py:
from django.urls import path, include
from .views import CustomTokenObtainPairView, CustomTokenRefreshView, LogoutView, ProfileView, CustomLoginView
urlpatterns = [
path('auth/', include('dj_rest_auth.urls')),
path('auth/registration/', include('dj_rest_auth.registration.urls')),
path('auth/login/', CustomLoginView.as_view(), name='rest_login'), # Custom login view
path('auth/token/', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'), # Custom token obtain view
path('auth/token/refresh/', CustomTokenRefreshView.as_view(), name='token_refresh'), # Custom token refresh view
path('auth/logout/', LogoutView.as_view(), name='rest_logout'), # Custom logout view
path('profile/', ProfileView.as_view(), name='profile'), # Profile view
]
accounts/views.py:
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
from rest_framework_simplejwt.exceptions import InvalidToken, TokenError
from django.http import JsonResponse
from dj_rest_auth.views import LoginView as DjRestAuthLoginView
from rest_framework import status
from django.utils.decorators import method_decorator
from django.views.decorators.debug import sensitive_post_parameters
class CustomTokenObtainPairView(TokenObtainPairView):
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
tokens = response.data
access_token = tokens['access']
refresh_token = tokens['refresh']
response = JsonResponse({'detail': 'Login successful'})
response.set_cookie(
key='access_token',
value=access_token,
httponly=True,
secure=True,
samesite='Lax'
)
response.set_cookie(
key='refresh_token',
value=refresh_token,
httponly=True,
secure=True,
samesite='Lax'
)
return response
class CustomTokenRefreshView(TokenRefreshView):
def post(self, request, *args, **kwargs):
refresh_token = request.COOKIES.get('refresh_token')
if not refresh_token:
return JsonResponse({'detail': 'Refresh token not provided'}, status=400)
serializer = self.get_serializer(data={'refresh': refresh_token})
try:
serializer.is_valid(raise_exception=True)
except TokenError as e:
raise InvalidToken(e.args[0])
access_token = serializer.validated_data['access']
response = JsonResponse({'detail': 'Token refreshed'})
response.set_cookie(
key='access_token',
value=access_token,
httponly=True,
secure=True,
samesite='Lax'
)
return response
class CustomLoginView(DjRestAuthLoginView):
@method_decorator(sensitive_post_parameters('password'))
def dispatch(self, *args, **kwargs):
return super().dispatch(*args, **kwargs)
def post(self, request, *args, **kwargs):
response = super().post(request, *args, **kwargs)
if response.status_code == 200:
# If login is successful, return a JSON response
return JsonResponse({
'detail': 'Login successful',
'user': request.user.username
}, status=status.HTTP_200_OK)
else:
# If login fails, return the response as is
return response