权限控制改造(菜单权限 + 按钮权限 + API权限)

143 阅读4分钟

前言

上一篇文章 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列表判断,可支持多个权限全部判断

感谢🙏阅读,自己留坑自己填