如何在TypeScript项目中基于Axios与React Context实现规范的身份认证管理?
Hey there, let's work through your questions step by step with practical fixes for your React + TypeScript auth setup:
1. Do I need to call setToken() when the user logs in?
Absolutely—this is critical for tying your auth state to Axios interceptors. Here's how it all fits together:
- When your login API call succeeds, it should return an access token (and ideally user info). You’ll need to call
setToken(accessToken)to update the auth state. - Your existing request interceptor already watches the
tokenstate—once you set it, every subsequent Axios request will automatically include theAuthorization: Bearer ${token}header, which is exactly what you want. - Pro tip: If your login endpoint returns user data alongside the token, you can immediately call
setUser(userData)too, instead of making a separate/auth/authorizecall right after login.
2. How to skip the /auth/authorize call on public routes (login/register)?
The issue right now is your useEffect runs every time the AuthProvider mounts, even on public pages where there’s no valid token (causing errors). Here’s how to fix it:
- Make sure your
AuthProvideris wrapped inside your React Router component so it can access route information. - Use
useLocationto check the current path, and skip the/auth/authorizecall if you’re on a public route.
Update your AuthProvider code to add this logic:
// Add this import at the top import { useLocation } from 'react-router-dom'; export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [token, setToken] = useState<string | null>(null); const [user, setUser] = useState<User | null>(null); const location = useLocation(); // Define your public routes here const publicRoutes = ['/login', '/register']; useEffect(() => { const fetchMe = async () => { try { const { data } = await axios.get( "http://localhost:5000/auth/authorize", { withCredentials: true }, ); setUser(data.user); } catch (err) { setUser(null); } }; // Only run the auth check if we're NOT on a public route if (!publicRoutes.includes(location.pathname)) { fetchMe(); } }, [location.pathname]); // Re-run when the route changes // ... rest of your existing code };
3. What’s the safest way to persist tokens between page refreshes?
You’re right to avoid localStorage—it’s vulnerable to XSS attacks since any JavaScript on the page can read it. The recommended approach is:
- Refresh Token: Store this in an HttpOnly, Secure, SameSite=Strict cookie (set by your backend). This cookie can’t be accessed by frontend JS, so it’s safe from XSS.
- Access Token: Keep this in React state (memory only). It will be lost on page refresh, but you can use the refresh token to get a new access token automatically.
Here’s how to adjust your code to implement this:
- Update the initial auth setup to try refreshing the token on page load (for non-public routes):
useEffect(() => { const initializeAuth = async () => { try { // First, use the refresh token (stored in HttpOnly cookie) to get a new access token const refreshRes = await axios.post( "/auth/refreshMyToken", {}, { withCredentials: true } ); const newToken = refreshRes.data.token; setToken(newToken); // Then fetch the user info with the new access token const { data } = await axios.get( "http://localhost:5000/auth/authorize", { headers: { Authorization: `Bearer ${newToken}` } } ); setUser(data.user); } catch (err) { // Refresh failed—no valid session, reset state setToken(null); setUser(null); } }; const publicRoutes = ['/login', '/register']; if (!publicRoutes.includes(location.pathname)) { initializeAuth(); } }, [location.pathname]);
- Fix your response interceptor to avoid infinite loops when refreshing tokens:
useLayoutEffect(() => { const responseInterceptor = axios.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; // Prevent infinite retry loops by marking requests that have already tried to refresh if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshRes = await axios.post( "/auth/refreshMyToken", {}, { withCredentials: true } ); const newToken = refreshRes.data.token; setToken(newToken); // Update the original request's auth header originalRequest.headers.Authorization = `Bearer ${newToken}`; // Retry the original request return axios(originalRequest); } catch (refreshErr) { // Refresh failed—log the user out logout(); return Promise.reject(refreshErr); } } // For non-401 errors or failed refreshes, log out and reject logout(); return Promise.reject(error); }, ); return () => { axios.interceptors.response.eject(responseInterceptor); }; }, []);
Full Modified Code
import axios from "axios"; import React, { createContext, useContext, useEffect, useLayoutEffect, useState } from "react"; import { useLocation } from 'react-router-dom'; type User = { id: string; username: string; email: string; }; type AuthContextType = { user: User | null; token: string | null; setToken: (token: string | null) => void; logout: () => void; }; const AuthContext = createContext<AuthContextType | undefined>(undefined); export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { const [token, setToken] = useState<string | null>(null); const [user, setUser] = useState<User | null>(null); const location = useLocation(); const publicRoutes = ['/login', '/register']; useEffect(() => { const initializeAuth = async () => { try { // Refresh token to get new access token const refreshRes = await axios.post( "/auth/refreshMyToken", {}, { withCredentials: true } ); const newToken = refreshRes.data.token; setToken(newToken); // Fetch user info with the new token const { data } = await axios.get( "http://localhost:5000/auth/authorize", { headers: { Authorization: `Bearer ${newToken}` } } ); setUser(data.user); } catch (err) { setToken(null); setUser(null); } }; if (!publicRoutes.includes(location.pathname)) { initializeAuth(); } }, [location.pathname]); useLayoutEffect(() => { const requestInterceptor = axios.interceptors.request.use( (config) => { if (token && config.headers) { config.headers.Authorization = `Bearer ${token}`; } return config; }, (error) => Promise.reject(error), ); return () => { axios.interceptors.request.eject(requestInterceptor); }; }, [token]); useLayoutEffect(() => { const responseInterceptor = axios.interceptors.response.use( (response) => response, async (error) => { const originalRequest = error.config; if (error.response?.status === 401 && !originalRequest._retry) { originalRequest._retry = true; try { const refreshRes = await axios.post( "/auth/refreshMyToken", {}, { withCredentials: true } ); const newToken = refreshRes.data.token; setToken(newToken); originalRequest.headers.Authorization = `Bearer ${newToken}`; return axios(originalRequest); } catch (refreshErr) { logout(); return Promise.reject(refreshErr); } } logout(); return Promise.reject(error); }, ); return () => { axios.interceptors.response.eject(responseInterceptor); }; }, []); const logout = () => { setToken(null); setUser(null); // Optional: Call your backend logout endpoint to invalidate the refresh token axios.post("/auth/logout", {}, { withCredentials: true }).catch(() => {}); }; return ( <AuthContext.Provider value={{ user, token, setToken, logout }}> {children} </AuthContext.Provider> ); }; export const useAuth = (): AuthContextType => { const context = useContext(AuthContext); if (!context) { throw new Error("useAuth must be used within an AuthProvider"); } return context; };
内容的提问来源于stack exchange,提问作者JesSy
相关产品推荐
相关产品推荐

