You've already forked DataMate
feat(auth): 完善API网关JWT认证和权限控制功能
- 实现网关侧JWT工具类和权限规则匹配器 - 集成JWT认证流程,支持Bearer Token验证 - 添加基于路径和HTTP方法的权限控制机制 - 配置白名单路由规则,优化认证性能 - 更新前端受保护路由组件,实现权限验证 - 添加403禁止访问页面和权限检查逻辑 - 重构登录页面,集成实际认证API调用 - 实现用户信息获取和权限加载功能 - 优化全局异常处理器中的认证错误状态码 - 集成FastJSON2和JJWT依赖库支持
This commit is contained in:
75
frontend/src/auth/permissions.ts
Normal file
75
frontend/src/auth/permissions.ts
Normal file
@@ -0,0 +1,75 @@
|
||||
export const PermissionCodes = {
|
||||
dataManagementRead: "module:data-management:read",
|
||||
dataManagementWrite: "module:data-management:write",
|
||||
dataAnnotationRead: "module:data-annotation:read",
|
||||
dataAnnotationWrite: "module:data-annotation:write",
|
||||
dataCollectionRead: "module:data-collection:read",
|
||||
dataCollectionWrite: "module:data-collection:write",
|
||||
dataEvaluationRead: "module:data-evaluation:read",
|
||||
dataEvaluationWrite: "module:data-evaluation:write",
|
||||
dataSynthesisRead: "module:data-synthesis:read",
|
||||
dataSynthesisWrite: "module:data-synthesis:write",
|
||||
knowledgeManagementRead: "module:knowledge-management:read",
|
||||
knowledgeManagementWrite: "module:knowledge-management:write",
|
||||
knowledgeBaseRead: "module:knowledge-base:read",
|
||||
knowledgeBaseWrite: "module:knowledge-base:write",
|
||||
operatorMarketRead: "module:operator-market:read",
|
||||
operatorMarketWrite: "module:operator-market:write",
|
||||
orchestrationRead: "module:orchestration:read",
|
||||
orchestrationWrite: "module:orchestration:write",
|
||||
contentGenerationUse: "module:content-generation:use",
|
||||
agentUse: "module:agent:use",
|
||||
userManage: "system:user:manage",
|
||||
roleManage: "system:role:manage",
|
||||
permissionManage: "system:permission:manage",
|
||||
} as const;
|
||||
|
||||
const routePermissionRules: Array<{ prefix: string; permission: string }> = [
|
||||
{ prefix: "/data/management", permission: PermissionCodes.dataManagementRead },
|
||||
{ prefix: "/data/annotation", permission: PermissionCodes.dataAnnotationRead },
|
||||
{ prefix: "/data/collection", permission: PermissionCodes.dataCollectionRead },
|
||||
{ prefix: "/data/evaluation", permission: PermissionCodes.dataEvaluationRead },
|
||||
{ prefix: "/data/synthesis", permission: PermissionCodes.dataSynthesisRead },
|
||||
{ prefix: "/data/knowledge-management", permission: PermissionCodes.knowledgeManagementRead },
|
||||
{ prefix: "/data/knowledge-base", permission: PermissionCodes.knowledgeBaseRead },
|
||||
{ prefix: "/data/operator-market", permission: PermissionCodes.operatorMarketRead },
|
||||
{ prefix: "/data/orchestration", permission: PermissionCodes.orchestrationRead },
|
||||
{ prefix: "/data/content-generation", permission: PermissionCodes.contentGenerationUse },
|
||||
{ prefix: "/chat", permission: PermissionCodes.agentUse },
|
||||
];
|
||||
|
||||
const defaultRouteCandidates: Array<{ path: string; permission: string }> = [
|
||||
{ path: "/data/management", permission: PermissionCodes.dataManagementRead },
|
||||
{ path: "/data/annotation", permission: PermissionCodes.dataAnnotationRead },
|
||||
{ path: "/data/knowledge-management", permission: PermissionCodes.knowledgeManagementRead },
|
||||
{ path: "/data/knowledge-base", permission: PermissionCodes.knowledgeBaseRead },
|
||||
{ path: "/chat", permission: PermissionCodes.agentUse },
|
||||
];
|
||||
|
||||
export function hasPermission(
|
||||
userPermissions: string[] | undefined,
|
||||
requiredPermission?: string | null
|
||||
): boolean {
|
||||
if (!requiredPermission) {
|
||||
return true;
|
||||
}
|
||||
return (userPermissions ?? []).includes(requiredPermission);
|
||||
}
|
||||
|
||||
export function resolveRequiredPermissionByPath(pathname: string): string | null {
|
||||
if (pathname === "/403") {
|
||||
return null;
|
||||
}
|
||||
const matchedRule = routePermissionRules.find((rule) =>
|
||||
pathname.startsWith(rule.prefix)
|
||||
);
|
||||
return matchedRule?.permission ?? null;
|
||||
}
|
||||
|
||||
export function resolveDefaultAuthorizedPath(userPermissions: string[]): string {
|
||||
const matchedPath = defaultRouteCandidates.find((candidate) =>
|
||||
hasPermission(userPermissions, candidate.permission)
|
||||
)?.path;
|
||||
return matchedPath ?? "/403";
|
||||
}
|
||||
|
||||
@@ -1,20 +1,53 @@
|
||||
import React from 'react';
|
||||
import { Navigate, useLocation, Outlet } from 'react-router';
|
||||
import { useAppSelector } from '@/store/hooks';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { fetchCurrentUser, markInitialized } from '@/store/slices/authSlice';
|
||||
import {
|
||||
hasPermission,
|
||||
resolveDefaultAuthorizedPath,
|
||||
resolveRequiredPermissionByPath,
|
||||
} from '@/auth/permissions';
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const ProtectedRoute: React.FC<ProtectedRouteProps> = ({ children }) => {
|
||||
const { isAuthenticated } = useAppSelector((state) => state.auth);
|
||||
const dispatch = useAppDispatch();
|
||||
const { isAuthenticated, token, initialized, loading, permissions } = useAppSelector(
|
||||
(state) => state.auth
|
||||
);
|
||||
const location = useLocation();
|
||||
const requiredPermission = resolveRequiredPermissionByPath(location.pathname);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (initialized || loading) {
|
||||
return;
|
||||
}
|
||||
if (!token) {
|
||||
dispatch(markInitialized());
|
||||
return;
|
||||
}
|
||||
void dispatch(fetchCurrentUser());
|
||||
}, [dispatch, initialized, loading, token]);
|
||||
|
||||
if (!initialized || loading) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
// Redirect to the login page, but save the current location they were trying to go to
|
||||
return <Navigate to="/login" state={{ from: location }} replace />;
|
||||
}
|
||||
|
||||
if (!hasPermission(permissions, requiredPermission)) {
|
||||
const fallbackPath = resolveDefaultAuthorizedPath(permissions);
|
||||
if (location.pathname === fallbackPath) {
|
||||
return <Navigate to="/403" replace />;
|
||||
}
|
||||
return <Navigate to={fallbackPath} replace />;
|
||||
}
|
||||
|
||||
return children ? <>{children}</> : <Outlet />;
|
||||
};
|
||||
|
||||
|
||||
24
frontend/src/pages/Forbidden/ForbiddenPage.tsx
Normal file
24
frontend/src/pages/Forbidden/ForbiddenPage.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import React from "react";
|
||||
import { Button, Result } from "antd";
|
||||
import { useNavigate } from "react-router";
|
||||
|
||||
const ForbiddenPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="h-screen w-full flex items-center justify-center bg-[#050b14]">
|
||||
<Result
|
||||
status="403"
|
||||
title="403"
|
||||
subTitle="你当前账号没有访问该页面的权限。"
|
||||
extra={
|
||||
<Button type="primary" onClick={() => navigate("/data/management")}>
|
||||
返回首页
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ForbiddenPage;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { memo, useCallback, useEffect, useState } from "react";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Button, Drawer, Menu, Popover } from "antd";
|
||||
import {
|
||||
CloseOutlined,
|
||||
@@ -14,6 +14,7 @@ import SettingsPage from "../SettingsPage/SettingsPage";
|
||||
import { useAppSelector, useAppDispatch } from "@/store/hooks";
|
||||
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
|
||||
import { logout } from "@/store/slices/authSlice";
|
||||
import { hasPermission } from "@/auth/permissions";
|
||||
|
||||
const isPathMatch = (currentPath: string, targetPath: string) =>
|
||||
currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
|
||||
@@ -25,13 +26,36 @@ const AsiderAndHeaderLayout = () => {
|
||||
const [sidebarOpen, setSidebarOpen] = useState(true);
|
||||
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
|
||||
const settingVisible = useAppSelector((state) => state.settings.visible);
|
||||
const permissions = useAppSelector((state) => state.auth.permissions);
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const visibleMenuItems = useMemo(
|
||||
() =>
|
||||
menuItems
|
||||
.map((item) => ({
|
||||
...item,
|
||||
children: item.children?.filter((subItem) =>
|
||||
hasPermission(permissions, (subItem as { permissionCode?: string }).permissionCode)
|
||||
),
|
||||
}))
|
||||
.filter((item) => {
|
||||
const selfVisible = hasPermission(
|
||||
permissions,
|
||||
(item as { permissionCode?: string }).permissionCode
|
||||
);
|
||||
if (item.children && item.children.length > 0) {
|
||||
return selfVisible;
|
||||
}
|
||||
return selfVisible;
|
||||
}),
|
||||
[permissions]
|
||||
);
|
||||
|
||||
// Initialize active item based on current pathname
|
||||
const initActiveItem = useCallback(() => {
|
||||
const dataPath = pathname.startsWith("/data/") ? pathname.slice(6) : pathname;
|
||||
for (let index = 0; index < menuItems.length; index++) {
|
||||
const element = menuItems[index];
|
||||
for (let index = 0; index < visibleMenuItems.length; index++) {
|
||||
const element = visibleMenuItems[index];
|
||||
if (element.children) {
|
||||
for (const subItem of element.children) {
|
||||
if (isPathMatch(dataPath, subItem.id)) {
|
||||
@@ -44,7 +68,8 @@ const AsiderAndHeaderLayout = () => {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}, [pathname]);
|
||||
setActiveItem(visibleMenuItems[0]?.id ?? "");
|
||||
}, [pathname, visibleMenuItems]);
|
||||
|
||||
useEffect(() => {
|
||||
initActiveItem();
|
||||
@@ -100,7 +125,7 @@ const AsiderAndHeaderLayout = () => {
|
||||
<Menu
|
||||
mode="inline"
|
||||
inlineCollapsed={!sidebarOpen}
|
||||
items={menuItems.map((item) => ({
|
||||
items={visibleMenuItems.map((item) => ({
|
||||
key: item.id,
|
||||
label: item.title,
|
||||
icon: item.icon ? <item.icon className="w-4 h-4" /> : null,
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
// Store,
|
||||
// Merge,
|
||||
} from "lucide-react";
|
||||
import { PermissionCodes } from "@/auth/permissions";
|
||||
|
||||
export const menuItems = [
|
||||
// {
|
||||
@@ -26,6 +27,7 @@ export const menuItems = [
|
||||
id: "management",
|
||||
title: "数集管理",
|
||||
icon: FolderOpen,
|
||||
permissionCode: PermissionCodes.dataManagementRead,
|
||||
description: "创建、导入和管理数据集",
|
||||
color: "bg-blue-500",
|
||||
},
|
||||
@@ -33,6 +35,7 @@ export const menuItems = [
|
||||
id: "annotation",
|
||||
title: "数据标注",
|
||||
icon: Tag,
|
||||
permissionCode: PermissionCodes.dataAnnotationRead,
|
||||
description: "对数据进行标注和标记",
|
||||
color: "bg-green-500",
|
||||
},
|
||||
@@ -40,6 +43,7 @@ export const menuItems = [
|
||||
id: "content-generation",
|
||||
title: "内容生成",
|
||||
icon: Sparkles,
|
||||
permissionCode: PermissionCodes.contentGenerationUse,
|
||||
description: "智能内容生成与创作",
|
||||
color: "bg-purple-500",
|
||||
},
|
||||
@@ -47,6 +51,7 @@ export const menuItems = [
|
||||
id: "knowledge-management",
|
||||
title: "知识管理",
|
||||
icon: Shield,
|
||||
permissionCode: PermissionCodes.knowledgeManagementRead,
|
||||
description: "管理知识集与知识条目",
|
||||
color: "bg-indigo-500",
|
||||
},
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React, { useState } from 'react';
|
||||
import React from 'react';
|
||||
import { useNavigate, useLocation } from 'react-router';
|
||||
import { Form, Input, Button, Typography, message, Card } from 'antd';
|
||||
import { Form, Input, Button, Typography, message } from 'antd';
|
||||
import { UserOutlined, LockOutlined } from '@ant-design/icons';
|
||||
import { useAppDispatch, useAppSelector } from '@/store/hooks';
|
||||
import { loginLocal } from '@/store/slices/authSlice';
|
||||
import { loginUser } from '@/store/slices/authSlice';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
@@ -11,19 +11,20 @@ const LoginPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const dispatch = useAppDispatch();
|
||||
const { loading, error } = useAppSelector((state) => state.auth);
|
||||
const { loading } = useAppSelector((state) => state.auth);
|
||||
const [messageApi, contextHolder] = message.useMessage();
|
||||
|
||||
const from = location.state?.from?.pathname || '/data';
|
||||
|
||||
const onFinish = (values: any) => {
|
||||
dispatch(loginLocal(values));
|
||||
// The reducer updates state synchronously.
|
||||
if (values.username === 'admin' && values.password === '123456') {
|
||||
messageApi.success('登录成功');
|
||||
navigate(from, { replace: true });
|
||||
} else {
|
||||
messageApi.error('账号或密码错误');
|
||||
const onFinish = async (values: { username: string; password: string }) => {
|
||||
try {
|
||||
await dispatch(loginUser(values)).unwrap();
|
||||
messageApi.success('登录成功');
|
||||
navigate(from, { replace: true });
|
||||
} catch (loginError) {
|
||||
const messageText =
|
||||
typeof loginError === 'string' ? loginError : '账号或密码错误';
|
||||
messageApi.error(messageText);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -59,9 +60,9 @@ const LoginPage: React.FC = () => {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
<Form<{ username: string; password: string }>
|
||||
name="login"
|
||||
initialValues={{ remember: true, username: 'admin', password: '123456' }}
|
||||
initialValues={{ username: 'admin', password: '123456' }}
|
||||
onFinish={onFinish}
|
||||
layout="vertical"
|
||||
size="large"
|
||||
|
||||
@@ -51,6 +51,7 @@ import Home from "@/pages/Home/Home";
|
||||
import ContentGenerationPage from "@/pages/ContentGeneration/ContentGenerationPage";
|
||||
import LoginPage from "@/pages/Login/LoginPage";
|
||||
import ProtectedRoute from "@/components/ProtectedRoute";
|
||||
import ForbiddenPage from "@/pages/Forbidden/ForbiddenPage";
|
||||
|
||||
const router = createBrowserRouter([
|
||||
{
|
||||
@@ -64,6 +65,10 @@ const router = createBrowserRouter([
|
||||
{
|
||||
Component: ProtectedRoute,
|
||||
children: [
|
||||
{
|
||||
path: "/403",
|
||||
Component: ForbiddenPage,
|
||||
},
|
||||
{
|
||||
path: "/chat",
|
||||
Component: withErrorBoundary(AgentPage),
|
||||
@@ -299,4 +304,4 @@ const router = createBrowserRouter([
|
||||
}
|
||||
]);
|
||||
|
||||
export default router;
|
||||
export default router;
|
||||
|
||||
@@ -1,66 +1,124 @@
|
||||
// store/slices/authSlice.js
|
||||
import { createSlice, createAsyncThunk } from '@reduxjs/toolkit';
|
||||
import { createAsyncThunk, createSlice } from "@reduxjs/toolkit";
|
||||
import { get, post } from "@/utils/request";
|
||||
|
||||
// 异步 thunk
|
||||
export const loginUser = createAsyncThunk(
|
||||
'auth/login',
|
||||
async (credentials, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = await fetch('/api/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify(credentials),
|
||||
});
|
||||
interface AuthUserView {
|
||||
id: number;
|
||||
username: string;
|
||||
fullName?: string;
|
||||
email?: string;
|
||||
avatarUrl?: string;
|
||||
organization?: string;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Login failed');
|
||||
}
|
||||
interface AuthLoginPayload {
|
||||
token: string;
|
||||
tokenType: string;
|
||||
expiresInSeconds: number;
|
||||
user: AuthUserView;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(error.message);
|
||||
}
|
||||
interface AuthCurrentUserPayload {
|
||||
user: AuthUserView;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
interface ApiResponse<T> {
|
||||
code: string;
|
||||
message: string;
|
||||
data: T;
|
||||
}
|
||||
|
||||
interface AuthState {
|
||||
user: AuthUserView | null;
|
||||
token: string | null;
|
||||
roles: string[];
|
||||
permissions: string[];
|
||||
isAuthenticated: boolean;
|
||||
initialized: boolean;
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
interface LoginCredentials {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const extractErrorMessage = (error: unknown): string => {
|
||||
if (error instanceof Error) {
|
||||
const nestedMessage = (error as { data?: { message?: string } }).data?.message;
|
||||
return nestedMessage ?? error.message;
|
||||
}
|
||||
);
|
||||
return "登录失败,请稍后重试";
|
||||
};
|
||||
|
||||
export const loginUser = createAsyncThunk<
|
||||
AuthLoginPayload,
|
||||
LoginCredentials,
|
||||
{ rejectValue: string }
|
||||
>("auth/login", async (credentials, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = (await post("/api/auth/login", credentials)) as ApiResponse<AuthLoginPayload>;
|
||||
if (!response?.data?.token) {
|
||||
return rejectWithValue(response?.message ?? "登录失败");
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(extractErrorMessage(error));
|
||||
}
|
||||
});
|
||||
|
||||
export const fetchCurrentUser = createAsyncThunk<
|
||||
AuthCurrentUserPayload,
|
||||
void,
|
||||
{ rejectValue: string }
|
||||
>("auth/fetchCurrentUser", async (_, { rejectWithValue }) => {
|
||||
try {
|
||||
const response = (await get("/api/auth/me")) as ApiResponse<AuthCurrentUserPayload>;
|
||||
if (!response?.data?.user) {
|
||||
return rejectWithValue(response?.message ?? "用户信息加载失败");
|
||||
}
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
return rejectWithValue(extractErrorMessage(error));
|
||||
}
|
||||
});
|
||||
|
||||
const initialToken = localStorage.getItem("token");
|
||||
|
||||
const initialState: AuthState = {
|
||||
user: null,
|
||||
token: initialToken,
|
||||
roles: [],
|
||||
permissions: [],
|
||||
isAuthenticated: Boolean(initialToken),
|
||||
initialized: false,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
const authSlice = createSlice({
|
||||
name: 'auth',
|
||||
initialState: {
|
||||
user: null,
|
||||
token: localStorage.getItem('token'),
|
||||
isAuthenticated: !!localStorage.getItem('token'),
|
||||
loading: false,
|
||||
error: null,
|
||||
},
|
||||
name: "auth",
|
||||
initialState,
|
||||
reducers: {
|
||||
logout: (state) => {
|
||||
state.user = null;
|
||||
state.token = null;
|
||||
state.roles = [];
|
||||
state.permissions = [];
|
||||
state.isAuthenticated = false;
|
||||
localStorage.removeItem('token');
|
||||
state.error = null;
|
||||
state.initialized = true;
|
||||
localStorage.removeItem("token");
|
||||
},
|
||||
clearError: (state) => {
|
||||
state.error = null;
|
||||
},
|
||||
setToken: (state, action) => {
|
||||
state.token = action.payload;
|
||||
localStorage.setItem('token', action.payload);
|
||||
},
|
||||
loginLocal: (state, action) => {
|
||||
const { username, password } = action.payload;
|
||||
if (username === 'admin' && password === '123456') {
|
||||
state.user = { username: 'admin', role: 'admin' };
|
||||
state.token = 'mock-token-' + Date.now();
|
||||
state.isAuthenticated = true;
|
||||
localStorage.setItem('token', state.token);
|
||||
state.error = null;
|
||||
} else {
|
||||
state.error = 'Invalid credentials';
|
||||
state.isAuthenticated = false;
|
||||
}
|
||||
markInitialized: (state) => {
|
||||
state.initialized = true;
|
||||
},
|
||||
},
|
||||
extraReducers: (builder) => {
|
||||
@@ -71,18 +129,52 @@ const authSlice = createSlice({
|
||||
})
|
||||
.addCase(loginUser.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.initialized = true;
|
||||
state.user = action.payload.user;
|
||||
state.token = action.payload.token;
|
||||
state.roles = action.payload.roles ?? [];
|
||||
state.permissions = action.payload.permissions ?? [];
|
||||
state.isAuthenticated = true;
|
||||
localStorage.setItem('token', action.payload.token);
|
||||
state.error = null;
|
||||
localStorage.setItem("token", action.payload.token);
|
||||
})
|
||||
.addCase(loginUser.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.error = action.payload;
|
||||
state.initialized = true;
|
||||
state.user = null;
|
||||
state.roles = [];
|
||||
state.permissions = [];
|
||||
state.isAuthenticated = false;
|
||||
state.token = null;
|
||||
state.error = action.payload ?? "登录失败";
|
||||
localStorage.removeItem("token");
|
||||
})
|
||||
.addCase(fetchCurrentUser.pending, (state) => {
|
||||
state.loading = true;
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchCurrentUser.fulfilled, (state, action) => {
|
||||
state.loading = false;
|
||||
state.initialized = true;
|
||||
state.user = action.payload.user;
|
||||
state.roles = action.payload.roles ?? [];
|
||||
state.permissions = action.payload.permissions ?? [];
|
||||
state.isAuthenticated = Boolean(state.token);
|
||||
state.error = null;
|
||||
})
|
||||
.addCase(fetchCurrentUser.rejected, (state, action) => {
|
||||
state.loading = false;
|
||||
state.initialized = true;
|
||||
state.user = null;
|
||||
state.roles = [];
|
||||
state.permissions = [];
|
||||
state.isAuthenticated = false;
|
||||
state.token = null;
|
||||
state.error = action.payload ?? "登录状态已失效";
|
||||
localStorage.removeItem("token");
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
export const { logout, clearError, setToken, loginLocal } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
export const { logout, clearError, markInitialized } = authSlice.actions;
|
||||
export default authSlice.reducer;
|
||||
|
||||
@@ -524,8 +524,16 @@ request.addRequestInterceptor((config) => {
|
||||
|
||||
// 添加默认响应拦截器 - 错误处理
|
||||
request.addResponseInterceptor((response) => {
|
||||
// 可以在这里添加全局错误处理逻辑
|
||||
// 比如token过期自动跳转登录页等
|
||||
if (response.status === 401) {
|
||||
localStorage.removeItem("token");
|
||||
sessionStorage.removeItem("token");
|
||||
if (window.location.pathname !== "/login") {
|
||||
window.location.href = "/login";
|
||||
}
|
||||
}
|
||||
if (response.status === 403 && window.location.pathname !== "/403") {
|
||||
window.location.href = "/403";
|
||||
}
|
||||
return response;
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user