You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React Hook顶层调用错误修复及SpringBoot会话管理方案求助

问题修复与优化方案

一、修复React Hook调用错误

你的核心错误是自定义Hook useSetupInterceptors在非组件/非Hook的顶层文件中调用,违反了React Hook的规则:Hook只能在React函数组件或自定义Hook内部调用。以下是具体修复步骤:

1. 重构EndpointConfig.js

移除顶层Hook调用,只保留配置和动态创建axios实例的逻辑:

import axios from 'axios';

const config = window.APP_CONFIG;
const endPointURLIP = config.endPointIP || "localhost";

// 从cookie获取token的工具函数
export const getTokenFromCookie = () => {
    const cookies = document.cookie.split(';').map(cookie => cookie.trim());
    for (const cookie of cookies) {
        if (cookie.startsWith('_auth=')) {
            const tokenCookie = cookie.split('=');
            if (tokenCookie.length === 2) {
                return tokenCookie[1];
            }
        }
    }
    return null;
};

// 动态创建axios实例,每次调用都会获取最新token
export const createAuthAxios = () => {
    const API_BASE_URL = `http://${endPointURLIP}:9081`;
    const authToken = getTokenFromCookie();
    const AUTHENTICATION_HEADER_TOKEN = authToken ? `Bearer ${authToken}` : null;

    return axios.create({
        baseURL: API_BASE_URL,
        headers: {
            Authorization: AUTHENTICATION_HEADER_TOKEN,
        },
    });
};

const endpointConfig = {
    BASE_URL: `http://${endPointURLIP}:9081`,
    HISTORY_ENDPOINT: '/demo/history',
};

export default endpointConfig;

2. 重构拦截器为工具函数

将useSetupInterceptors改为普通工具函数,避免Hook调用限制:

// useSetupInterceptors.js
export const setupInterceptors = (axiosInstance, signOut, navigate) => {
    const interceptor = axiosInstance.interceptors.response.use(
        (response) => response,
        (error) => {
            if (error.response && error.response.status === 403) {
                console.error('会话失效,请重新登录:', error.message);
                handleLogout(signOut, navigate);
            }
            return Promise.reject(error);
        }
    );

    const handleLogout = (signOut, navigate) => {
        signOut();
        navigate('/');
    };

    // 返回移除拦截器的函数,用于组件卸载时清理
    return () => {
        axiosInstance.interceptors.response.eject(interceptor);
    };
};

3. 在App组件中初始化拦截器

在AuthProvider上下文内调用useSignOut,并设置全局axios拦截器:

// App.js
import React, { useEffect } from 'react';
import { BrowserRouter, Route, Routes, useNavigate } from 'react-router-dom';
import { AuthProvider, RequireAuth, useSignOut } from 'react-auth-kit';
import DemoHistory from './components/DemoHistory';
import { createAuthAxios } from './services/EndpointConfig';
import { setupInterceptors } from './util/useSetupInterceptors';

const AppContent = () => {
    const signOut = useSignOut();
    const navigate = useNavigate();

    useEffect(() => {
        const authAxios = createAuthAxios();
        const ejectInterceptor = setupInterceptors(authAxios, signOut, navigate);
        
        // 将axios实例挂载到全局,方便服务层调用
        window.authAxios = authAxios;
        
        return () => ejectInterceptor();
    }, [signOut, navigate]);

    return (
        <div>
            <h2 className="header_name">Demo</h2>
            <BrowserRouter>
                <Routes>
                    <Route path="/DemoHistory" element={<RequireAuth loginPath="/"><DemoHistory /></RequireAuth>} />
                </Routes>
            </BrowserRouter>
        </div>
    );
};

function App() {
    return (
        <AuthProvider
            authType={"cookie"}
            authName={"_auth"}
            cookieDomain={window.location.hostname}
            cookieSecure={false}
        >
            <AppContent />
        </AuthProvider>
    );
}

export default App;

4. 修改DemoService使用动态实例

确保每次请求都使用最新的token:

// DemoService.js
import endpointConfig from './EndpointConfig';
import { createAuthAxios } from './EndpointConfig';

class DemoService {
    constructor() {
        this.getHistoryEndpoint = endpointConfig.HISTORY_ENDPOINT;
    }

    async getDemoHistory() {
        try {
            const authAxios = createAuthAxios();
            const response = await authAxios.get(`${this.getHistoryEndpoint}`);
            return response.data;
        } catch (error) {
            console.error('Error retrieving data:', error);
            throw error; // 抛出错误让组件处理
        }
    }
}

export default new DemoService();

二、会话管理优化方案

1. 动态Token获取机制

每次请求前重新从cookie读取token,避免初始化时的token因Java重启、tokenVersion失效而过期。

2. 浏览器关闭时清理会话

监听beforeunload事件,在用户关闭浏览器/标签页时主动清理会话:

// 在AppContent组件中添加
useEffect(() => {
    const handleBeforeUnload = () => {
        signOut();
    };

    window.addEventListener('beforeunload', handleBeforeUnload);
    return () => {
        window.removeEventListener('beforeunload', handleBeforeUnload);
    };
}, [signOut]);

同时可以在后端设置cookie的maxAge属性,确保会话自动过期,双重保障。

3. 403错误体验优化

替换alert为更友好的提示方式,同时清除本地缓存:

// 修改setupInterceptors中的handleLogout
const handleLogout = (signOut, navigate) => {
    // 清除额外本地存储
    localStorage.removeItem('userInfo');
    signOut();
    // 可使用react-toastify等组件展示提示
    // toast.error('会话已失效,请重新登录');
    setTimeout(() => navigate('/'), 1000);
};

三、额外建议

  • 避免使用window.location.href跳转,改用react-router-dom的useNavigate钩子,保持单页应用路由一致性。
  • 实现Token自动刷新:在token过期前请求新的token,减少用户登录频率。
  • 添加全局错误边界组件,捕获组件树中的错误,提升用户体验。

内容的提问来源于stack exchange,提问作者Harish Mahi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.27 02:19:56