一、展示控制
前端权限控制的目的是,根据当前用户的身份控制其能访问的页面和可执行的操作。需要注意的是:前端权限控制主要是为了提升用户体验(如隐藏无权限的菜单,按钮),正真的数据安全必须依赖后端实现。
二、RBAC
业界主流的权限管理模型是RBAC(基于角色的访问控制),其核心思想是将"权限"授予"角色",将"角色"授予"用户",实现了用户与权限的逻辑分离,极大的简化了权限的分配与管理。
三、主要流程
主要包括用户身份认证、权限分配、权限校验和页面展示控制。
- 用户登录后,前端从后端获取用户的权限列表。
- 前端根据用户权限信息,决定展示哪些菜单或按钮。
- 路由级别做权限校验,未授权用户访问受限页面时自动跳转到无权限提示页或登录页。
- 组件级别做权限控制,操作按钮或表单项根据权限动态展示或禁用。
四、实现要点
1.获取用户权限信息
// context/AuthProvider
const AuthContext = createContext(undefined);
export const useAuth = () => useContext(AuthContext);
export const AuthProvider = ({ children }) => {
const [user, setUser] = useState(null);
// 从本地存储中恢复用户权限信息
useEffect(() => {
const user = localStorage.getItem('user');
if (user) {
setUser(JSON.parse(user));
}
}, []);
const login = async (username, password) => {
const user = await loginApi(username,password);
setUser(user);
// 登录后缓存用户权限信息
localStorage.setItem('user', JSON.stringify(user));
};
const logout = () => {
setUser(null);
// 登出后清除本地缓存
localStorage.removeItem('user');
};
const hasPermission = (permission: string | string[]): boolean => {
if (!user) return false;
if (Array.isArray(permission)) {
return permission.some(p => user.permissions.includes(p));
}
return user.permissions.includes(permission);
};
const value = {
user,
login,
logout,
hasPermission
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
2.封装路由权限校验组件
// components/AuthRoute.js
import { useAuth } from '../context/AuthProvider'; // 自定义 hook,获取用户信息
const AuthRoute = ({ children, meta }) => {
const { user, hasPermission } = useAuth();
// 用户未登录,重定向到登录页面
if (meta.requiresAuth && !user) {
return <Navigate to="/login" replace />;
}
// 用户没有权限,重定向到未授权页面
if (meta.permission && !hasPermission(meta.permission)) {
return <Navigate to="/403" replace />;
}
// 权限通过,渲染子组件
return children;
};
export default AuthRoute;
3.创建路由
// router/index.js
import AuthRoute from '../components/AuthRoute';
const Router = () => {
const element = routes.map(({ path, element:Component, meta }) => ({
path,
element: (
<AuthRoute meta={meta}>
<Component />
</AuthRoute>
)
}));
return <RouterProvider router={createBrowserRouter(routers)} />;
};
export default Router;
4.封装按钮权限校验组件
import { useAuth } from '../context/AuthProvider'; // 自定义 hook,获取用户信息
export const AuthButton = ({
permission,
children,
onClick,
}) => {
const { hasPermission } = useAuth();
const hasAccess = hasPermission(permission);
if (!hasAccess) {
return null;
}
return (
<button
onClick={onClick}
>
{children}
</button>
);
};
5.按钮权限控制
import { AuthButton } from '../components/AuthButton';
export const ContentManagement = () => {
return (
<AuthButton
permission="content.edit"
onClick={() => handleEdit(item.id)}
>
编辑
</AuthButton>
);
};
五、技术难点
1.多粒度权限控制
- 页面级权限控制:通过前端路由守卫实现,例如,React Router的高阶组件、Vue Router 的beforeEach钩子。
- 组件级权限控制:通过条件渲染隐藏或禁用无权限的按钮。
2.细粒度权限控制
按钮、表单项等细粒度权限控制,难点在于检查点分散,如果每个按钮都要添加额外的权限控制逻辑,维护成本高;另外权限检查函数频繁执行(如在列表中渲染几十个按钮),可能造成性能问题。
常用的做法是封装自定义 Hook(如 usePermission)或高阶组件,并且缓存组件的权限检查结果。
3.状态管理的复杂性
用户权限信息需要全局共享且保持一致性。难点在于:
- 初始化时机:页面渲染时可能还没拿到用户信息,容易导致未授权页面闪现。
- Token 过期:接口返回Token过期,需要自动跳转登录,同时清空本地缓存。
- 多标签页同步:如果一个标签页登出,其他标签页也需要更新状态,否则可能操作报错。
解决方案通常是利用 Context全局共享,使用webStorage本地缓存,利用广播实现多标签页同步。
4.前后端权限一致性
前端权限控制本质是提升用户体验,正真的数据安全必须依赖后端实现。但难点在于:
- 双重校验的一致性:前端隐藏了按钮,用户仍可能通过直接访问 API 进行操作,所以后端必须对所有接口做权限校验。
- 数据同步滞后:如果后端修改了用户权限,前端可能仍保留旧的权限缓存,导致用户看到不应看到的操作或无法访问新功能。需要设计合适的刷新机制(如定时拉取、权限变更后强制刷新)。