前言
上一篇文章 React 18 全局错误捕获 + React-router 6 动态权限路由 经过实践和思考后,发现有一些问题 针对上一篇文章总结下:
旧版菜单缓存方案
- 实现方式 :登录后获取用户对应权限菜单和权限列表,存储到Redux和localStorage中
- 优点 :刷新页面无需重新请求,性能好,用户体验流畅
- 缺点 :安全隐患 ,用户可通过修改localStorage中的菜单数据绕过权限验证,加载未授权的页面组件,需结合API权限控制才能避免
痛定思痛,决定重构
新方案
权限存储策略
- 仅在localStorage中存储权限标识列表和过期时间戳
- 不存储完整的菜单结构和路由配置
- 服务端为权限数据生成签名,客户端可验证数据完整性
菜单获取机制
- 每次刷新页面时,通过API重新获取菜单数据
- 服务端根据用户权限和角色动态生成可访问的菜单结构
- 支持条件请求,仅在权限变化时返回新数据
安全增强措施
- 实现JWT Token验证,带有合理的过期时间
- 关键操作添加API级别的权限二次验证
- 实现权限变更的实时通知机制
性能优化策略
- 为菜单请求添加短期缓存(如5-10分钟)
- 实现增量更新,只传输变化的部分
- 使用Redis等缓存服务减轻后端压力
接下来按新方案实施
菜单权限
一、移除旧版redux里浏览器缓存相关
移除旧的浏览器缓存,增加权限的permission和permissionTimestamp缓存,新的代码如下
import { createSlice } from '@reduxjs/toolkit'
import { isDataInLocalStorageAndNotEmpty } from "@/utils/utils";
// 缓存的userinfo
const userinfo = isDataInLocalStorageAndNotEmpty('userinfo')?JSON.parse(localStorage.getItem('userinfo')):{};
// 缓存权限
const catchPermission = isDataInLocalStorageAndNotEmpty('permission') ?JSON.parse(localStorage.getItem('permission')):[];
export const globalSlice = createSlice({
name: 'global',
initialState: {
// 用户信息
userInfo: {...userinfo},
// 菜单
menus:[],
// 按钮权限信息
permission:[...catchPermission],
// 权限时间戳
permissionTimestamp: localStorage.getItem('permissionTimestamp') || Date.now(),
// 动态加载页面
asyncfiles:[],
// progress状态
isLoading:true
},
reducers: {
initmenus: (state, action) => {
state.menus = action.payload
},
setmenus: (state, action) => {
// 只有当数据真正变化时才更新状态和本地存储
if (JSON.stringify(state.menus) !== JSON.stringify(action.payload)) {
state.menus = action.payload;
}
},
setpermissions: (state, action) => {
// 只有当数据真正变化时才更新状态和本地存储
if (JSON.stringify(state.permission) !== JSON.stringify(action.payload)) {
state.permission = action.payload;
}
},
setasyncfiles: (state, action) => {
state.asyncfiles = action.payload
},
login: (state, action) => {
state.userInfo = action.payload.userinfo
localStorage.setItem('userinfo', JSON.stringify(state.userInfo));
localStorage.setItem('access_token', action.payload.accessToken);
localStorage.setItem('refresh_token', action.payload.refreshToken);
},
logout: (state) => {
state.userInfo = {};
state.menus = [];
state.permission = {};
localStorage.clear();
}
}
})
export const { login, logout ,setmenus,setasyncfiles,setpermissions} = globalSlice.actions
export default globalSlice.reducer
二、增加AuthRoute组件
代码如下
import { useEffect } from 'react';
import { useLocation, useNavigate } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { checkPermission } from '@/utils/utils';
export const AuthRoute = ({ children }) => {
const location = useLocation();
const navigate = useNavigate();
const { permission, asyncfiles } = useSelector(state => state.global);
useEffect(() => {
// 非登录页面需要验证权限
if (location.pathname !== '/' && location.pathname !== '/login' && location.pathname !== '/404' && location.pathname !== '/403') {
// 从asyncfiles中查找当前路由对应的权限
const currentRoute = asyncfiles.find(item => item.path === location.pathname);
const requiredPermission = currentRoute?.permission;
// 如果权限不存在也跳转到403页面
if (!requiredPermission) {
navigate('/403', { replace: true });
}
// 如果路由需要权限且用户没有该权限
if (requiredPermission && !checkPermission(requiredPermission, permission)) {
navigate('/403', { replace: true });
}
}
}, [location.pathname, navigate, permission, asyncfiles]);
return children;
};
// checkPermission工具函数
export function checkPermission(requiredPermission, permissions) {
// 支持字符串和数组类型的权限检查
if (!requiredPermission) return true;
if (typeof requiredPermission === 'string') {
return permissions.includes(requiredPermission);
}
if (Array.isArray(requiredPermission)) {
// 支持多种权限模式:全部满足(AND)或任一满足(OR)
return requiredPermission.some(perm => permissions.includes(perm));
}
return false;
}
三、增加usePermission权限验证HOOK
import { useSelector } from 'react-redux';
import { checkPermission } from '@/utils/utils';
export const usePermission = () => {
const permissions = useSelector(state => state.global.permission);
const hasPermission = (requiredPermission) => {
return checkPermission(requiredPermission, permissions);
};
const showByPermission = (requiredPermission, element, fallbackElement = null) => {
return hasPermission(requiredPermission) ? element : fallbackElement;
};
return {
hasPermission,
showByPermission,
permissions
};
};
四、路由配置
useEffect(() => {
if (asyncfiles.length > 0) {
const newAsyncRouter = asyncfiles
.map((item) => {
const Comp = components[item.filepath];
if (!Comp) return null;
return {
path: item.path,
element: lazyComponent(Comp)
};
})
.filter(Boolean);
const updatedRoutes = [...routers];
updatedRoutes[0].children.push(...newAsyncRouter);
setRoutes(updatedRoutes);
return
}
fetchMenus(dispatch);
}, [asyncfiles]);
其他文件见上一篇文章
按钮权限
增加AuthButton组件
/**
* @file 权限组件
* @date 2025/11/07 19:11:16
* @author lyqjob@yeah.net
*/
import React from 'react';
import { useSelector } from 'react-redux';
/**
* @param {*} children 子元素
* @param {*} permission 权限
* @param {*} fallback 无权限时的回退元素
* @param {*} mode 权限模式,'all': 所有权限都需要, 'any': 任一权限即可
* @returns
*/
export const AuthComponent = ({
children,
permission,
fallback = null,
mode = 'all' // 'all': 所有权限都需要, 'any': 任一权限即可
}) => {
const userPermissions = useSelector(state => state.global.permission || []);
// 检查权限
const hasAccess = React.useMemo(() => {
if (!permission) return true;
if (Array.isArray(permission)) {
if (mode === 'all') {
return permission.every(p => userPermissions.includes(p));
} else {
return permission.some(p => userPermissions.includes(p));
}
} else {
return userPermissions.includes(permission);
}
}, [permission, userPermissions, mode]);
return hasAccess ? children : fallback;
};
/**
* @file 权限按钮组件
* @date 2025/11/07 19:11:16
* @author lyqjob@yeah.net
*/
import React from 'react';
import { AuthComponent } from './AuthComponent';
/**
* @param {*} children 子元素
* @param {*} permission 权限
* @param {*} fallback 无权限时的回退元素
* @param {*} mode 权限模式,'all': 所有权限都需要, 'any': 任一权限即可
* @returns
*/
const AuthButton = ({ children, permission, fallback = null, mode = 'all' }) => {
return (
<AuthComponent permission={permission} fallback={fallback} mode={mode}>
{children}
</AuthComponent>
);
};
export default AuthButton;
通过后端返回的permission列表判断,可支持多个权限全部判断
感谢🙏阅读,自己留坑自己填