前端权限控制怎样设计路由、按钮和接口三层?
前端根据服务端下发的权限码控制路由可达、菜单与按钮的显示,以及请求拦截后的统一处理;这些都只是展示控制,可以被绕过,真正的授权必须由服务端对每个请求、每个资源校验。
参考答案
简答
- 前端权限只做展示控制:隐藏无权访问的菜单、按钮,拦截无权进入的路由,目的是体验,不是安全。
- 代码、接口地址和请求都在用户手里,前端判断可以被绕过;服务端必须对每个请求做授权,并检查具体资源是否属于当前用户。
- 权限数据由服务端下发(角色或权限码列表),前端不硬编码"某角色能做什么",权限变化时只改服务端配置。
- 接口层统一处理 401 和 403:401 表示未登录或登录过期,跳转登录;403 表示无权限,给出提示并刷新权限数据。
权限数据
JavaScript
// 登录后从服务端获取
const session = { userId: 'u1', permissions: ['order:read', 'order:export'] };
const can = (code) => session.permissions.includes(code);
用细粒度权限码而不是角色名做判断:页面只关心"能否导出订单",不关心用户是管理员还是运营。角色与权限码的映射留在服务端。
缺少权限码时默认不可见、不可进,与 OWASP 建议的"默认拒绝"一致。
路由层
- 路由配置上声明所需权限码,全局导航守卫中检查,不满足则跳转 403 页或首页。
- 菜单从同一份路由配置生成,避免菜单可见但进入后被拦的不一致。
- 动态添加路由(只注册有权限的路由)可以让无权路由彻底不存在,但对应的代码分包仍可能被下载和阅读,因此也不构成安全保障。
权限在使用中变化(被管理员收回)或导航并发时的处理见《路由并发导航和权限变化应该怎样处理?》。
按钮层
JSX
function Can({ code, children, fallback = null }) {
return can(code) ? children : fallback;
}
<Can code="order:export">
<button onClick={exportOrders}>导出</button>
</Can>;
封装成组件或指令,避免权限判断散落在各处。对重要但无权限的操作,可以显示禁用状态并说明原因,而不是让功能神秘消失,按产品需求选择。
接口层
JavaScript
async function request(url, init) {
const res = await fetch(url, init);
if (res.status === 401) redirectToLogin();
if (res.status === 403) {
toast('没有权限执行该操作');
refreshPermissions(); // 权限可能已变化,同步最新数据
}
return res;
}
前端在接口层能做的是统一的错误处理,不是授权。服务端要在每个请求上校验:当前用户是否有该操作权限,以及目标对象是否在其范围内。只校验"已登录"而不校验"这张订单是不是你的",会产生越权访问漏洞。
数据层面的限制
隐藏字段不等于保护字段。接口如果把手机号、成本价返回给无权用户,前端不显示也能在开发者工具里看到。敏感字段应由服务端按权限裁剪后再返回。
常见追问
- 权限数据存在
localStorage被改了怎么办?只会让用户看到更多按钮,点击后接口仍会返回 403,这正说明授权必须在服务端。 - 页面级权限能用 JWT 里的声明吗?可以用于展示判断,但服务端校验时以服务端的权限数据为准,并考虑令牌未过期但权限已变更的情况。
- 多租户系统要注意什么?服务端每个查询都要带租户条件,前端传来的租户 ID 不能直接信任。