前端权限缓存设计与实现踩坑总结

小亚楠 安全 阅读 1,534
赞 22 收藏
二维码
手机扫码查看
反馈

权限缓存的核心代码就这么几行

最近重构了一个老项目,权限管理这块之前写得乱七八糟,每次进入页面都重新请求权限数据,用户体验差得要命。这次直接搞了个权限缓存系统,效果立竿见影。

前端权限缓存设计与实现踩坑总结

先上核心代码:

class PermissionCache {
  constructor() {
    this.cacheKey = 'user_permissions';
    this.expireTime = 24 * 60 * 60 * 1000; // 24小时过期
  }

  // 获取权限数据
  async getPermissions(forceRefresh = false) {
    const cached = this.getCachedData();
    
    if (cached && !this.isExpired(cached.timestamp) && !forceRefresh) {
      return cached.data;
    }

    // 缓存失效或强制刷新时重新获取
    const permissions = await this.fetchFromServer();
    this.setCachedData(permissions);
    return permissions;
  }

  getCachedData() {
    try {
      const cached = localStorage.getItem(this.cacheKey);
      return cached ? JSON.parse(cached) : null;
    } catch (error) {
      console.error('读取权限缓存失败:', error);
      return null;
    }
  }

  setCachedData(data) {
    const cacheData = {
      data,
      timestamp: Date.now()
    };
    
    try {
      localStorage.setItem(this.cacheKey, JSON.stringify(cacheData));
    } catch (error) {
      console.error('保存权限缓存失败:', error);
    }
  }

  isExpired(timestamp) {
    return Date.now() - timestamp > this.expireTime;
  }

  clearCache() {
    localStorage.removeItem(this.cacheKey);
  }

  async fetchFromServer() {
    const response = await fetch('/api/user/permissions', {
      headers: {
        'Authorization': Bearer ${localStorage.getItem('token')}
      }
    });
    
    if (!response.ok) {
      throw new Error('获取权限失败');
    }
    
    return response.json();
  }
}

这个类很简单,就是把权限数据缓存在 localStorage 里,设置个过期时间。关键在于时间控制和错误处理,这两个地方我踩过不少坑。

Vue项目中的具体应用

在 Vue 项目里,我是这么用的:

// permissionManager.js
import PermissionCache from './PermissionCache';

const permissionCache = new PermissionCache();

export default {
  // 初始化权限
  async initPermissions() {
    try {
      const permissions = await permissionCache.getPermissions();
      this.permissions = permissions;
      this.updateRouteAccess();
    } catch (error) {
      console.error('权限初始化失败:', error);
      // 清除缓存并重试
      permissionCache.clearCache();
    }
  },

  // 检查是否有某项权限
  hasPermission(permissionCode) {
    if (!this.permissions) {
      console.warn('权限数据未加载');
      return false;
    }
    return this.permissions.includes(permissionCode);
  },

  // 更新路由访问权限
  updateRouteAccess() {
    // 根据权限动态更新路由
    this.updateRoutesBasedOnPermissions();
  }
};

// 在 main.js 中全局注册
import permissionManager from './utils/permissionManager';

// 路由守卫中使用
router.beforeEach(async (to, from, next) => {
  if (to.meta.requiresAuth) {
    await permissionManager.initPermissions();
    
    if (to.meta.permission && !permissionManager.hasPermission(to.meta.permission)) {
      next('/unauthorized'); // 没有权限跳转到无权页面
    } else {
      next();
    }
  } else {
    next();
  }
});

这样做的好处很明显:用户第一次登录后,后续页面切换都不需要重复请求权限数据,响应速度快了很多。

踩坑提醒:这三点一定注意

这里要重点说几个坑,都是我亲自踩过的:

  • 缓存大小限制:localStorage 有容量限制,一般5MB左右。权限数据如果太复杂,建议只缓存核心的权限码数组,不要把整个用户信息都塞进去。
  • 并发请求处理:多个组件同时请求权限数据时,会出现多次请求的问题。我加了个 pending 状态:
class PermissionCache {
  constructor() {
    this.cacheKey = 'user_permissions';
    this.expireTime = 24 * 60 * 60 * 1000;
    this.pendingPromise = null; // 用于防止并发请求
  }

  async getPermissions(forceRefresh = false) {
    const cached = this.getCachedData();
    
    if (cached && !this.isExpired(cached.timestamp) && !forceRefresh) {
      return cached.data;
    }

    // 如果已经有请求在进行中,直接返回之前的 promise
    if (this.pendingPromise && !forceRefresh) {
      return this.pendingPromise;
    }

    // 创建新的请求 promise
    this.pendingPromise = this.fetchFromServer()
      .then(permissions => {
        this.setCachedData(permissions);
        this.pendingPromise = null;
        return permissions;
      })
      .catch(error => {
        this.pendingPromise = null;
        throw error;
      });

    return this.pendingPromise;
  }
}
  • 权限变更同步:用户权限修改后,缓存要及时清理。我在权限修改接口后加上了缓存清理:
// 权限修改成功后清除缓存
async updateUserPermission(userId, permissions) {
  const response = await fetch(/api/users/${userId}/permissions, {
    method: 'PUT',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': Bearer ${localStorage.getItem('token')}
    },
    body: JSON.stringify({ permissions })
  });

  if (response.ok) {
    // 清除权限缓存,下次进入时重新获取
    permissionCache.clearCache();
  }
  
  return response;
}

React项目里的实现方式

React 项目中我用自定义 Hook 的方式实现:

// hooks/usePermissions.js
import { useState, useEffect } from 'react';

function usePermissions() {
  const [permissions, setPermissions] = useState(null);
  const [loading, setLoading] = useState(false);

  const loadPermissions = async () => {
    setLoading(true);
    try {
      const cached = getCachedPermissions();
      
      if (cached && !isCacheExpired(cached.timestamp)) {
        setPermissions(cached.data);
      } else {
        const data = await fetchPermissions();
        setCachedPermissions(data);
        setPermissions(data);
      }
    } catch (error) {
      console.error('加载权限失败:', error);
    } finally {
      setLoading(false);
    }
  };

  useEffect(() => {
    loadPermissions();
  }, []);

  const hasPermission = (permissionCode) => {
    return permissions?.includes(permissionCode) || false;
  };

  return { permissions, loading, hasPermission, refresh: loadPermissions };
}

// 在组件中使用
function ProtectedComponent() {
  const { hasPermission, loading } = usePermissions();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (!hasPermission('view_dashboard')) {
    return <div>无访问权限</div>;
  }

  return <div>Dashboard Content</div>;
}

React 的方式更符合其数据流的设计理念,状态管理和副作用处理都比较清晰。

高级玩法:内存+本地双重缓存

有些项目对性能要求更高,我会做双重缓存:内存 + 本地存储。内存缓存优先级最高,避免重复计算:

class AdvancedPermissionCache {
  constructor() {
    this.memoryCache = new Map(); // 内存缓存
    this.localStorageKey = 'user_permissions';
    this.cacheVersion = 'v1.0'; // 缓存版本,方便清空旧缓存
  }

  async getPermissions() {
    // 1. 先检查内存缓存
    if (this.memoryCache.has('permissions')) {
      return this.memoryCache.get('permissions');
    }

    // 2. 检查本地缓存
    const localCache = this.getLocalCache();
    if (localCache) {
      // 设置内存缓存
      this.memoryCache.set('permissions', localCache.data);
      return localCache.data;
    }

    // 3. 请求服务器数据
    const serverData = await this.fetchFromServer();
    this.setLocalCache(serverData);
    this.memoryCache.set('permissions', serverData);
    
    return serverData;
  }

  getLocalCache() {
    try {
      const cached = localStorage.getItem(this.localStorageKey);
      if (!cached) return null;

      const parsed = JSON.parse(cached);
      if (parsed.version !== this.cacheVersion) {
        // 版本不匹配,清除缓存
        this.clearLocalCache();
        return null;
      }

      return parsed;
    } catch (error) {
      console.error('读取本地缓存失败:', error);
      return null;
    }
  }

  setLocalCache(data) {
    const cacheData = {
      version: this.cacheVersion,
      data,
      timestamp: Date.now()
    };

    try {
      localStorage.setItem(this.localStorageKey, JSON.stringify(cacheData));
    } catch (error) {
      console.error('保存本地缓存失败:', error);
    }
  }

  clearLocalCache() {
    localStorage.removeItem(this.localStorageKey);
  }

  // 页面卸载时清理内存缓存
  static cleanup() {
    // 这里可以做一些清理工作
  }
}

这种双重缓存的方式,对于频繁访问权限数据的场景特别有效,内存级别的缓存查询几乎无延迟。

缓存策略的选择很重要

缓存策略选择直接影响用户体验和系统性能。我一般根据业务特点来决定:

实时性要求高的场景:比如金融系统,权限变更需要立即生效,缓存时间设置短一些(5-10分钟)或者提供手动刷新机制。

一般业务系统:缓存24小时基本够用,用户一天内再次访问不需要重新获取权限。

离线应用场景:需要考虑网络不稳定的情况,缓存时间可以适当延长,并配合降级策略。

还有个细节需要注意:不同角色的用户权限缓存应该区分,不能混用。我的做法是在缓存key中加入用户ID:

// 用户隔离的缓存key
getUserCacheKey(userId) {
  return user_permissions_${userId};
}

这个技巧的拓展用法还有很多,后续会继续分享这类博客。以上是我踩坑后的总结,希望对你有帮助。

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

暂无评论