/** * 客户端认证守卫组件 * * 在客户端检查 localStorage 中的 token * 如果未认证且访问的是需要认证的路径,则跳转到登录页 */ import { useEffect } from 'react'; import { useNavigate, useLocation } from '@remix-run/react'; import { isAuthenticated } from '~/utils/auth-storage'; interface ClientAuthGuardProps { isPublicPath: boolean; frontendJWT?: string; userInfo?: Record; } export function ClientAuthGuard({ isPublicPath, frontendJWT, userInfo }: ClientAuthGuardProps) { const navigate = useNavigate(); const location = useLocation(); useEffect(() => { console.log('🔍 [Auth Guard] useEffect 触发', { isPublicPath, pathname: location.pathname }); // 如果是公共路径,不需要检查认证 if (isPublicPath) { console.log('✅ [Auth Guard] 公共路径,跳过认证检查'); return; } // 服务端如果已经拿不到有效 session,就不要再信任本地残留 token。 // 否则页面会出现“模块全空了”,但又没有跳回登录页的假登录状态。 if (!frontendJWT && !userInfo) { console.warn('⚠️ [Auth Guard] 服务端会话已失效,清理本地登录态并跳转登录页'); localStorage.removeItem('access_token'); localStorage.removeItem('user_info'); const redirectTo = `${location.pathname}${location.search}` || '/'; navigate(`/login?expired=true&redirect=${encodeURIComponent(redirectTo)}`, { replace: true }); return; } // 优先用服务端 session 回传的数据同步 localStorage。 // 不能只在本地没有 token 时才回填,否则本地残留旧 token 会导致: // - SSR 页面可打开(服务端 session 是新的) // - CSR 子页面请求 401(客户端 localStorage 还是旧的) const token = localStorage.getItem('access_token'); if (frontendJWT && token !== frontendJWT) { localStorage.setItem('access_token', frontendJWT); console.log('✅ [Auth Guard] 已根据服务端 session 同步最新 access_token'); } if (userInfo) { const serializedUserInfo = JSON.stringify(userInfo); if (localStorage.getItem('user_info') !== serializedUserInfo) { localStorage.setItem('user_info', serializedUserInfo); console.log('✅ [Auth Guard] 已根据服务端 session 同步最新 user_info'); } } const authenticated = isAuthenticated() || !!frontendJWT; if (!authenticated) { console.log('🔒 [Auth Guard] 未认证,重定向到登录页'); // 保存当前路径,登录后可以跳转回来 const redirectTo = location.pathname !== '/login' ? `${location.pathname}${location.search}` : '/'; // 跳转到登录页,并传递重定向目标 navigate(`/login?redirect=${encodeURIComponent(redirectTo)}`, { replace: true }); } else { // console.log('✅ [Auth Guard] 已认证,允许访问'); } }, [isPublicPath, navigate, location.pathname, location.search, frontendJWT, userInfo]); // 这个组件不渲染任何内容 return null; }