当前位置: 首页 > news >正文

芋道字段级权限扩展

开发背景

实习负责ERP系统销售模块,有生产成本等价格字段的权限控制需求,正好芋道不支持字段级权限控制,遂扩展支持了这部分权限系统。

实现方法

由于是 B 端传统软件,不存在明显的性能瓶颈,同时也为了保持业务代码的低侵入性,选择在 Spring MVC 响应处理链上注册一个 ControllerAdvice ,拦截带有
@FieldPermissionController 响应体传递,对返回的 VO 类进行反射操作,将权限控制的字段写 null,随后配合 @JsonInclude(JsonInclude.Include.NON_NULL) 注解,在序列化 Json 返回前端的时候过滤掉无权限字段,实现权限控制。

Controller 方法接收请求
方法或类
是否标注@FieldPermission?
直接返回正常响应
业务代码返回 VO/List/PageResult
进入 FieldPermissionAdvice
(ResponseBodyAdvice 拦截)
获取当前登录用户角色
查缓存获该角色下无权限字段集合
通过反射将无权限字段在 VO 上 set null
回传被清洗后的 VO/List/PageResult
Spring MVC 提交给 Jackson 序列化处理
VO 类上
有@JsonInclude.NON_NULL?
所有字段都输出(含 null)
为 null 字段被自动遗漏
(无权限字段在 JSON 中自动消失)
最终生成 JSON 响应,返回前端

缓存机制

通过 Caffeine + Redis + Mysql三级数据访问,保证鉴权性能。

缓存接口 FieldPermissionChecker
package cn.iocoder.yudao.module.system.fieldPermission;  import lombok.Getter;  import java.util.Set;  /**  * 字段权限检查器接口  * 用于判断某个角色是否有权访问某个 VO 的某个字段  */  
public interface FieldPermissionChecker {  /**  * 判断指定角色是否允许查看某 VO 的某字段  *  * @param roleCode      角色编码(如 2)  * @param voClassName   VO 类名(如 "cn.iocoder.yudao.module.sales.vo.CustomerVO")  * @param fieldName     字段名(如 "phone")  * @return 是否允许访问  */  boolean isFieldAllowed(Integer roleCode, String voClassName, String fieldName);  /**  * 批量判断多个字段是否允许访问(优化性能,减少多次 lookup)  *  * @param roleCode      角色编码  * @param voClassName   VO 类名  * @param fieldNames    字段名集合  * @return 不允许访问的字段集合  */  Set<String> checkNotAllowedFields(Integer roleCode, String voClassName, Set<String> fieldNames);  /**  * 获取该角色对该 VO 所有允许访问的字段(可用于预加载)  *  * @param roleCode      角色编码  * @param voClassName   VO 类名  * @return 不允许的字段集合  */  Set<String> getAllNotAllowedFields(Long roleCode, String voClassName);  /**  * 刷新缓存(当权限配置变更时调用)  * 实现类可根据需要清空本地缓存、删除 Redis 缓存等  */  void refreshCache();  /**  * 获取缓存统计信息(可选,用于监控)  *  * @return 统计信息(如命中率、大小等)  */  CacheStats getCacheStats();  /**  * 缓存统计内部类  */  @Getter  class CacheStats {  // getter  private final long hitCount;  private final long missCount;  private final long totalSize;  public CacheStats(long hitCount, long missCount, long totalSize) {  this.hitCount = hitCount;  this.missCount = missCount;  this.totalSize = totalSize;  }  public double getHitRate() {  long total = hitCount + missCount;  return total == 0 ? 0.0 : (double) hitCount / total;  }  }  
}
Caffeine

使用 Caffeine 作为本地内存缓存,减少 Redis 和数据库回源流量,维护Cache<FieldPermissionKey, Boolean>: (roleCode,voClassName, fieldName) 三元组结构和VO 对应的所有禁止字段两种缓存结构,即对应 VO 结构下该用户角色对某个字段是否具有访问权限。

参数配置:

// key: (role, vo, field) -> Boolean
// 单字段是否可见
private final Cache<FieldPermissionKey, Boolean> localCache;  
// key: (role, vo) -> Set<String>
// VO 表对应所有的禁止字段缓存
private final Cache<FieldPermissionKeyAll, Set<String>> notAllowedFieldsCache;public FieldPermissionCacheService() {  this.localCache = Caffeine.newBuilder()  .maximumSize(10_000)      .expireAfterAccess(10, TimeUnit.MINUTES)  .recordStats()  .build();  this.notAllowedFieldsCache = Caffeine.newBuilder()  .maximumSize(2_000)  .expireAfterAccess(10, TimeUnit.MINUTES)  .recordStats()  .build();  
}
  • maximumSize:缓存中允许的最大条目数,上限溢出后会移除最近最少使用的数据。
  • expireAfterAccess:某条数据如果在设定时间内没有被访问,就会自动过期并被移除。
  • expireAfterWrite:某条数据自写入(加入缓存)后,达到指定时间即失效(不管是否被访问过)。
  • recordStats:开启缓存的使用统计信息(如命中、未命中、移除次数等),方便性能监控。
  • initialCapacity:缓存初始化时分配的槽位大小,提升高并发场景下的初始性能。

由于单权限形式(roleId:ClassName:fieldName)结果会明显多于对 Set 集合的缓存,所以分配空间更大。

Redis

这里使用stringRedisTemplate操作 Redis,避免序列化、反序列化的麻烦。采用private final String REDIS_KEY_PREFIX = "field_permission:" 作为 Redis 命名空间。
单权限缓存使用 String 类型(TTL = 24H),VO 表组字段(TTL = 2H)缓存采用 Set 类型。

刷新缓存时,通过命名空间前缀模糊匹配批量删除 Redis 键:

@Override  
public void refreshCache() {  localCache.invalidateAll();  notAllowedFieldsCache.invalidateAll();  Set<String> keys1 = stringRedisTemplate.keys(REDIS_KEY_PREFIX + "*");  if (!keys1.isEmpty()) {  stringRedisTemplate.delete(keys1);  }  Set<String> keys2 = stringRedisTemplate.keys(REDIS_NOT_ALLOWED_SET_PREFIX + "*");  if (!keys2.isEmpty()) {  stringRedisTemplate.delete(keys2);  }  log.info("[refreshCache][刷新缓存成功]");  
}
http://www.dtcms.com/a/500180.html

相关文章:

  • P5283 十二省联考 2019 异或粽子
  • 抗干扰天线的相关的技术参数:干扰抑制能力,干信比
  • 制作板块的网站wordpress 获取当前文章栏目链接
  • Mysql笔记13
  • 企业网站优化的弊端重庆网站建设联系电话
  • Claude 新功能深度解析:Agent Skills 让 AI 智能体变得更专业
  • C++:类和对象(3)|初始化列表|类型转换
  • 小面网站建设长春优化
  • c++贪吃蛇V1.0
  • 南京量身营销型网站设计个人网站 做啥好
  • 每日算法刷题Day75:10.18:leetcode 二叉树14道题,用时3h
  • C语言解析json字符串
  • 网站在哪里设置域名站长统计免费下载
  • 街道门户网站的建设思路.net网站模版
  • MyBatis—XML配置
  • Microsoft AI Genius | 用 MCP 解锁实时数据,重新定义交互边界
  • 杨园建设社区网站wordpress digg主题
  • HarmonyOS6 应用升级经验分享
  • 核工业西南建设集团有限公司网站网站管理助手 二级域名
  • 十月通勤小记
  • 贵州住房城乡建设厅官方网站公司的网站
  • 绪论——一文速通
  • 皮卡丘密码
  • 聊城建设局网站做公司官网大概多少钱
  • 教育系统网站cms网站开发完整视频
  • 肇庆免费模板建站logo成品效果图网站
  • 电商网站的支付接入该怎么做呢wordpress静态分页
  • Vue2(一):创建实例、插值表达式、Vue响应式特性、Vue指令、指令修饰符、计算属性
  • 网站建设东莞公司湖南邵阳调整多个风险区
  • 10种常用的进制转换