权限管理是现代Web应用开发中的核心功能之一,本文将带你从理论到实践全面掌握权限管理,并结合React+Vite+TS技术栈进行代码实现。
一、权限管理基础理论
1.1 权限管理的基本概念
权限管理(Access Control)是指系统对用户访问资源的控制过程,主要包括:
-
认证(Authentication):验证用户身份(你是谁)
-
授权(Authorization):确定用户能做什么
1.2 常见的权限模型
-
RBAC(基于角色的访问控制)
-
用户关联角色,角色关联权限
-
例如:管理员、普通用户等角色
-
-
ABAC(基于属性的访问控制)
-
基于用户、资源、环境等属性动态判断
-
更灵活但实现复杂
-
-
ACL(访问控制列表)
-
直接定义用户对资源的操作权限
-
适用于简单系统
-
1.3 前端权限控制的核心要点
-
路由权限:控制用户能访问哪些页面
-
UI权限:控制页面中显示哪些元素
-
API权限:控制用户能调用哪些接口
-
数据权限:控制用户能看到哪些数据
二、React+Vite+TS权限管理实践
2.1 项目初始化
npm create vite@latest react-permission-demo --template react-ts
cd react-permission-demo
npm install
npm install react-router-dom @types/react-router-dom
2.2 路由权限控制
2.2.1 定义路由配置
// src/router/config.tsx
import { lazy } from 'react';
import { RouteObject } from 'react-router-dom';
const Login = lazy(() => import('@/pages/Login'));
const Dashboard = lazy(() => import('@/pages/Dashboard'));
const UserList = lazy(() => import('@/pages/UserList'));
const NotFound = lazy(() => import('@/pages/NotFound'));
export interface RouteMeta {
title?: string;
requiresAuth?: boolean; // 是否需要登录
roles?: string[]; // 可访问的角色
}
export interface AppRouteObject extends RouteObject {
meta?: RouteMeta;
children?: AppRouteObject[];
}
export const routes: AppRouteObject[] = [
{
path: '/login',
element: <Login />,
meta: {
title: '登录',
requiresAuth: false,
},
},
{
path: '/',
element: <Dashboard />,
meta: {
title: '首页',
requiresAuth: true,
roles: ['admin', 'user'],
},
},
{
path: '/user',
element: <UserList />,
meta: {
title: '用户管理',
requiresAuth: true,
roles: ['admin'],
},
},
{
path: '*',
element: <NotFound />,
meta: {
title: '404',
requiresAuth: false,
},
},
];
2.2.2 实现权限路由组件
// src/router/PermissionRoute.tsx
import { ReactNode, Suspense } from 'react';
import { Navigate, useLocation } from 'react-router-dom';
import { useAuth } from '@/hooks/useAuth';
import { AppRouteObject } from './config';
interface PermissionRouteProps {
route: AppRouteObject;
children: ReactNode;
}
export function PermissionRoute({ route, children }: PermissionRouteProps) {
const { isAuthenticated, userRoles } = useAuth();
const location = useLocation();
// 如果路由需要认证但用户未登录,重定向到登录页
if (route.meta?.requiresAuth && !isAuthenticated) {
return <Navigate to="/login" state={{ from: location }} replace />;
}
// 检查用户是否有访问该路由的角色权限
if (route.meta?.roles) {
const hasPermission = route.meta.roles.some(role =>
userRoles.includes(role)
);
if (!hasPermission) {
return <Navigate to="/403" replace />;
}
}
return <Suspense fallback={<div>Loading...</div>}>{children}</Suspense>;
}
2.2.3 实现路由渲染
// src/router/index.tsx
import { useRoutes } from 'react-router-dom';
import { routes } from './config';
import { PermissionRoute } from './PermissionRoute';
export function Router() {
const element = useRoutes(
routes.map(route => ({
path: route.path,
element: (
<PermissionRoute route={route}>
{route.element}
</PermissionRoute>
),
children: route.children,
}))
);
return element;
}
2.3 认证与权限状态管理
2.3.1 创建Auth Context
// src/context/AuthContext.tsx
import { createContext, useContext, ReactNode, useState } from 'react';
interface User {
id: string;
username: string;
roles: string[];
}
interface AuthContextType {
user: User | null;
isAuthenticated: boolean;
userRoles: string[];
login: (userData: User) => void;
logout: () => void;
}
const AuthContext = createContext<AuthContextType>(null!);
export function AuthProvider({ children }: { children: ReactNode }) {
const [user, setUser] = useState<User | null>(() => {
// 从localStorage初始化用户状态
const storedUser = localStorage.getItem('user');
return storedUser ? JSON.parse(storedUser) : null;
});
const login = (userData: User) => {
setUser(userData);
localStorage.setItem('user', JSON.stringify(userData));
};
const logout = () => {
setUser(null);
localStorage.removeItem('user');
};
const value = {
user,
isAuthenticated: !!user,
userRoles: user?.roles || [],
login,
logout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
export function useAuth() {
return useContext(AuthContext);
}
2.3.2 封装权限Hook
// src/hooks/usePermission.ts
import { useAuth } from '@/context/AuthContext';
export function usePermission() {
const { userRoles } = useAuth();
const hasPermission = (requiredRoles?: string[]) => {
if (!requiredRoles || requiredRoles.length === 0) return true;
return requiredRoles.some(role => userRoles.includes(role));
};
return { hasPermission };
}
2.4 UI权限控制
2.4.1 实现权限按钮组件
// src/components/PermissionButton.tsx
import { ReactNode } from 'react';
import { usePermission } from '@/hooks/usePermission';
interface PermissionButtonProps {
children: ReactNode;
requiredRoles?: string[];
fallback?: ReactNode;
}
export function PermissionButton({
children,
requiredRoles,
fallback = null,
}: PermissionButtonProps) {
const { hasPermission } = usePermission();
if (hasPermission(requiredRoles)) {
return <>{children}</>;
}
return <>{fallback}</>;
}
2.4.2 使用示例
// 在组件中使用
<PermissionButton
requiredRoles={['admin']}
fallback={<Tooltip title="需要管理员权限"><LockOutlined /></Tooltip>}
>
<Button type="primary">删除用户</Button>
</PermissionButton>
2.5 API权限拦截
2.5.1 封装axios实例
// src/api/request.ts
import axios from 'axios';
import { useAuth } from '@/context/AuthContext';
const instance = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000,
});
// 请求拦截器
instance.interceptors.request.use(
config => {
const { isAuthenticated, user } = useAuth.getState();
if (isAuthenticated && user?.token) {
config.headers.Authorization = `Bearer ${user.token}`;
}
return config;
},
error => {
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
response => {
return response.data;
},
error => {
if (error.response?.status === 401) {
// token过期或无效,跳转到登录页
useAuth.getState().logout();
window.location.href = '/login';
} else if (error.response?.status === 403) {
// 无权限访问
message.error('无权限访问该资源');
}
return Promise.reject(error);
}
);
export default instance;
2.6 动态菜单实现
2.6.1 根据权限过滤菜单
// src/hooks/useMenu.ts
import { useMemo } from 'react';
import { useAuth } from '@/context/AuthContext';
import { routes } from '@/router/config';
export function useMenu() {
const { userRoles } = useAuth();
const menuItems = useMemo(() => {
return routes
.filter(route => {
// 过滤掉不需要显示的菜单项
if (!route.meta || route.meta.hideInMenu) return false;
// 检查权限
if (!route.meta.roles) return true;
return route.meta.roles.some(role => userRoles.includes(role));
})
.map(route => ({
key: route.path,
label: route.meta?.title,
icon: route.meta?.icon,
}));
}, [userRoles]);
return menuItems;
}
三、高级权限管理技巧
3.1 按钮级权限控制优化
使用自定义指令或高阶组件实现更细粒度的控制:
// src/components/withPermission.tsx
import { usePermission } from '@/hooks/usePermission';
export function withPermission<T extends JSX.IntrinsicAttributes>(
WrappedComponent: React.ComponentType<T>,
requiredRoles?: string[]
) {
return function WithPermissionWrapper(props: T) {
const { hasPermission } = usePermission();
if (hasPermission(requiredRoles)) {
return <WrappedComponent {...props} />;
}
return null;
};
}
// 使用示例
const AdminButton = withPermission(Button, ['admin']);
<AdminButton type="primary">管理员按钮</AdminButton>
3.2 数据权限控制
// src/hooks/useDataPermission.ts
import { useAuth } from '@/context/AuthContext';
export function useDataPermission() {
const { user } = useAuth();
const filterData = <T extends { departmentId?: string }>(
data: T[],
field = 'departmentId'
): T[] => {
if (!user?.dataScope) return data;
// 根据用户的数据权限范围过滤数据
if (user.dataScope === 'all') return data;
if (user.dataScope === 'department') {
return data.filter(item => item[field] === user.departmentId);
}
if (user.dataScope === 'self') {
return data.filter(item => item.id === user.id);
}
return data;
};
return { filterData };
}
3.3 权限变更监听
// 在AuthContext中添加
useEffect(() => {
const handleStorageChange = (e: StorageEvent) => {
if (e.key === 'user') {
setUser(e.newValue ? JSON.parse(e.newValue) : null);
}
};
window.addEventListener('storage', handleStorageChange);
return () => window.removeEventListener('storage', handleStorageChange);
}, []);
四、安全最佳实践
-
前端权限只是体验优化:真正的权限控制必须在后端实现
-
最小权限原则:只授予用户完成工作所需的最小权限
-
定期审查权限:定期检查和清理不必要的权限
-
敏感操作记录日志:记录所有敏感操作以便审计
-
JWT安全:使用合理的过期时间,考虑刷新令牌机制
-
防范XSS:对所有用户输入进行转义处理
-
CSRF防护:使用CSRF令牌或SameSite Cookie属性
五、测试与调试
5.1 编写权限测试用例
// src/tests/permission.test.tsx
import { render, screen } from '@testing-library/react';
import { AuthProvider } from '@/context/AuthContext';
import { PermissionButton } from '@/components/PermissionButton';
test('PermissionButton shows content when user has permission', () => {
render(
<AuthProvider
value={{
user: { id: '1', username: 'admin', roles: ['admin'] },
isAuthenticated: true,
userRoles: ['admin'],
login: jest.fn(),
logout: jest.fn(),
}}
>
<PermissionButton requiredRoles={['admin']}>
<button>Admin Button</button>
</PermissionButton>
</AuthProvider>
);
expect(screen.getByText('Admin Button')).toBeInTheDocument();
});
test('PermissionButton shows fallback when user has no permission', () => {
render(
<AuthProvider
value={{
user: { id: '1', username: 'user', roles: ['user'] },
isAuthenticated: true,
userRoles: ['user'],
login: jest.fn(),
logout: jest.fn(),
}}
>
<PermissionButton
requiredRoles={['admin']}
fallback={<span>No Permission</span>}
>
<button>Admin Button</button>
</PermissionButton>
</AuthProvider>
);
expect(screen.queryByText('Admin Button')).not.toBeInTheDocument();
expect(screen.getByText('No Permission')).toBeInTheDocument();
});
六、总结
本文从权限管理的基础理论出发,详细讲解了在React+Vite+TS项目中实现完整权限控制系统的方案,包括:
-
路由权限控制
-
认证状态管理
-
UI元素权限控制
-
API权限拦截
-
数据权限过滤
-
高级权限技巧
记住,前端权限控制主要是为了提供更好的用户体验,真正的安全控制必须在后端实现。前后端必须协同工作,才能构建出安全可靠的权限管理系统。
完整项目示例可以参考:李阳/权限管理