权限刷新机制的设计与实战优化技巧
我的写法,亲测靠谱
权限刷新这事,说白了就是用户在页面上操作时,突然发现按钮点不了、菜单看不到了,或者数据接口403了——这时候你得让用户无感地重新拿一下权限配置,而不是直接弹个登录框把人踢出去。我一般这样处理:
// 权限刷新的核心逻辑,放在一个全局可调用的函数里
async function refreshPermissions() {
try {
const res = await fetch('https://jztheme.com/api/user/permissions', {
method: 'GET',
credentials: 'include',
headers: {
'Content-Type': 'application/json'
}
});
if (!res.ok) {
throw new Error(权限获取失败: ${res.status});
}
const data = await res.json();
window.currentPermissions = data.permissions; // 挂到全局方便检查
updateUIBasedOnPermissions(); // 根据新权限更新界面
return true;
} catch (err) {
console.error('刷新权限出错', err);
handleAuthFailure(); // 跳转登录或弹窗提示
return false;
}
}
这里的关键是:这个函数要能被任意组件主动触发,比如拦截器捕获到403后调一次,或者用户切换角色时手动调一次。不要依赖路由守卫去拉权限,因为用户可能在一个页面待很久,权限早就过期了。
然后我会配一个简单的权限校验工具:
function hasPermission(permissionKey) {
return window.currentPermissions?.includes?.(permissionKey);
}
组件里就这么用:
if (hasPermission('user.create')) {
showCreateButton();
} else {
hideCreateButton();
}
这种写法更靠谱,因为你在任何地方都能快速判断,不用层层传props或者搞复杂的store订阅。
这几种错误写法,别再踩坑了
我见过太多项目在这上面翻车,最典型的就是“只在登录时拿一次权限”。听起来省事,实际线上一堆问题:
- 用户A登录后拥有“编辑文章”权限,管理员后台把他的权限删了,但前端完全不知道,继续让A点编辑按钮,结果一堆403报错,用户体验极差
- 同一个账号切换角色(比如从普通员工切到管理员),页面不刷新就看不到新菜单,还得强制 reload,用户骂声一片
还有人喜欢在每个接口前面加个前置检查:
async function callApiWithPermissionCheck() {
await refreshPermissions(); // 每次都刷新!
return fetch('/api/data');
}
这种写法我折腾了半天才发现性能崩了——页面一打开十几个请求并发,每个都先刷一遍权限,服务器直接被打满。别这么干!
另一个常见错误是把权限存在 localStorage 里,登录时 setItem,后面一直 getItem 读。问题是 localStorage 不会自动同步多标签页状态。用户在一个标签页退出了,其他标签页还是显示有权限,点啥都报错,清缓存都救不了。
拦截器里的正确姿势
我现在标准做法是在 axios 拦截器里监听 401/403,并做防抖处理:
let isRefreshing = false;
let refreshSubscribers = [];
function subscribeTokenRefresh(cb) {
refreshSubscribers.push(cb);
}
function onRefreshed() {
refreshSubscribers.forEach((cb) => cb());
refreshSubscribers = [];
}
axios.interceptors.response.use(
(response) => response,
async (error) => {
const originalRequest = error.config;
if (error.response?.status === 403 && !originalRequest._retry) {
if (isRefreshing) {
// 如果已经在刷新,等待即可
return new Promise((resolve) => {
subscribeTokenRefresh(() => {
resolve(axios(originalRequest));
});
});
}
originalRequest._retry = true;
isRefreshing = true;
const success = await refreshPermissions();
isRefreshing = false;
onRefreshed();
if (success) {
return axios(originalRequest);
} else {
return Promise.reject(error);
}
}
return Promise.reject(error);
}
);
这段代码我用了三年,基本没出过大问题。关键点在于:防止单页面多个请求同时触发刷新、避免无限重试、保证后续请求能继续执行。
注意这里的 _retry 标志位,不加的话会陷入死循环——刷新后再发原请求,又进拦截器,又403……直到堆栈溢出。
实际项目中的坑
有一次上线后收到报警,大量用户卡在loading页不动。查日志发现 refreshPermissions() 接口502了,导致整个拦截器走不到 resolve,页面所有请求全挂起。后来加上了超时和降级:
const CONTROLLER = new AbortController();
const timeoutId = setTimeout(() => CONTROLLER.abort(), 8000);
try {
const res = await fetch('https://jztheme.com/api/user/permissions', {
signal: CONTROLLER.signal
});
// ...
} catch (err) {
if (err.name === 'AbortError') {
console.warn('权限刷新超时,使用缓存权限');
// 使用上次成功的权限数据,至少保证页面可用
} else {
handleAuthFailure();
}
}
哪怕接口暂时不可用,也要尽量保核心功能可用,不能因为权限问题让整个系统瘫痪。
还有一个细节:别在每次路由切换时都强制刷新权限。很多团队图省事,在 router.beforeEach 里直接 await refreshPermissions(),结果页面切换卡顿明显。应该只在特定条件下触发,比如距离上次刷新超过5分钟,或者明确知道权限可能变了(比如刚完成了角色切换操作)。
缓存与更新策略
我现在的方案是:内存中存一份 window.currentPermissions,localStorage 也存一份用于热刷新恢复。登录成功 or refreshPermissions 成功时双写,页面加载时从 localStorage 恢复:
// 页面初始化
const cached = localStorage.getItem('user_permissions');
if (cached) {
try {
window.currentPermissions = JSON.parse(cached);
updateUIBasedOnPermissions();
} catch (e) {}
}
这样即使网络抖动导致首次刷新失败,也能用旧权限撑一会儿,不至于白屏。
但是注意:localStorage 的数据必须设置过期时间,否则用户权限被回收后永远无法同步。我的做法是存的时候带个 timestamp:
localStorage.setItem(
'permissions_timestamp',
Date.now().toString()
);
刷新前检查是否超过 30 分钟,超过就强制走网络请求。
以上是我踩坑后的总结,希望对你有帮助
这套机制不是最优解,比如微前端环境下跨应用权限同步还是有点麻烦,目前靠事件广播 + LocalStorage 监听勉强够用。也有团队用 WebSocket 推送权限变更通知,但我觉着太重了,小项目没必要。
核心思想就一句:权限不是静态的,前端必须具备动态感知和恢复能力。别等到用户点不动了才想起来处理。
以上是我个人对权限刷新的完整实践分享,有更优的实现方式欢迎评论区交流。这类问题往往没有银弹,都是边上线边修出来的。

暂无评论