权限刷新机制的设计与实战优化技巧

书瑜 Dev 安全 阅读 966
赞 9 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

权限刷新这事,说白了就是用户在页面上操作时,突然发现按钮点不了、菜单看不到了,或者数据接口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 推送权限变更通知,但我觉着太重了,小项目没必要。

核心思想就一句:权限不是静态的,前端必须具备动态感知和恢复能力。别等到用户点不动了才想起来处理。

以上是我个人对权限刷新的完整实践分享,有更优的实现方式欢迎评论区交流。这类问题往往没有银弹,都是边上线边修出来的。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论