RBAC(基于角色的访问控制)和Redis的集成是个效率活儿,用Redis当"速查手册",把权限相关的"账本"记下来,省得每次都翻数据库那本"大账本",既快又省力。今天咱拿这段代码开刀,聊聊这俩是怎么搭伙的,还得加点料,把@CacheEvict
这个注解掰开了揉碎了讲清楚:它咋跟Redis关联的?它干活时看啥?它从哪儿来的,参数又是干啥的?细节拉满,不整那些云里雾里的术语。
RBAC和Redis是啥,干啥用的?
RBAC是个管权限的办法,核心思路是通过"角色"来分活儿。比如老板说:"管理员能看所有订单,普通用户只能看自己的。"不用给每个员工单独写权限表,给"管理员"和"用户"这两个角色分配权限,用户挂到角色下就行。省事,管理方便。
Redis是个跑得飞快的内存数据库,专门存经常要查的东西。RBAC里权限校验是个高频活儿,每次请求都得问"你有权吗",老去数据库问,累不说,速度还慢。Redis一出场,相当于把权限表抄到小本子上,查起来嗖嗖的。
这代码咋把RBAC和Redis绑一块儿了?
从代码看,系统用Redis缓存RBAC的权限数据,减少数据库压力。具体咋干的?咱分三块儿聊:缓存定义 、缓存操作 (重点讲@CacheEvict
)、权限校验。
1. 缓存定义:给Redis划好地盘
先看RbacCacheNames.java
,这块儿是给Redis里的数据起名的地方。用键值对方式,键名得有讲究,不能乱七八糟,不然找不着。代码里定了几个关键的"地盘":
ini
public interface RbacCacheNames {
String RBAC_PREFIX = "TestModule_rbac:"; // 前缀,像个门牌号
String PERMISSIONS_KEY = RBAC_PREFIX + RBAC_PREFIX + "permission:permissions:"; // 所有权限列表
String USER_PERMISSIONS_KEY = RBAC_PREFIX + "permission:user_permissions:"; // 用户的权限
String URI_PERMISSION_KEY = RBAC_PREFIX + "permission:uri_permissions:"; // URI对应的权限
String MENU_LIST_KEY = RBAC_PREFIX + "menu:list:"; // 菜单列表
String MENU_ID_LIST_KEY = RBAC_PREFIX + "menu:id_list:"; // 菜单ID列表
}
- RBAC_PREFIX:前缀"TestModule_rbac:",像小区名字,告诉Redis这是咱RBAC的地盘,别跟别人混了。
- 各种KEY :后面接具体名字,比如
USER_PERMISSIONS_KEY
实际用时可能是"TestModule_rbac:permission:user_permissions:用户ID",存的是某个用户的权限列表。
这设计就像给Redis分了几个抽屉,权限、菜单、URI映射各放各的,找起来方便。
2. 缓存操作:角色变了咋更新?(@CacheEvict全解析)
再看UserRoleFeignController.java
,这是管用户和角色关系的地方。角色变了(比如加个角色、改个角色),Redis里的缓存得跟着动,不然权限就乱套了。这块儿用到了@CacheEvict
注解,咱得好好聊聊。
less
@RestController
public class UserRoleFeignController implements UserRoleFeignClient {
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(cacheNames = CacheNames.MENU_ID_LIST_KEY, key = "#userRoleDTO.userId")
public ServerResponseEntity<Void> saveByUserIdAndSysType(UserRoleDTO userRoleDTO) {
userRoleMapper.insertUserAndUserRole(userRoleDTO.getUserId(), userRoleDTO.getRoleIds());
return ServerResponseEntity.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
@CacheEvict(cacheNames = CacheNames.MENU_ID_LIST_KEY, key = "#userRoleDTO.userId")
public ServerResponseEntity<Void> updateByUserIdAndSysType(UserRoleDTO userRoleDTO) {
userRoleMapper.deleteByUserId(userRoleDTO.getUserId());
userRoleMapper.insertUserAndUserRole(userRoleDTO.getUserId(), userRoleDTO.getRoleIds());
return ServerResponseEntity.success();
}
}
@CacheEvict
是啥?咋跟Redis关联的?
@CacheEvict
是个"清道夫",告诉系统:这个方法跑完后,把Redis里指定的缓存踢掉。咋跟Redis关联的呢?它其实是Spring框架的缓存抽象(Spring Cache)的一部分。Spring Cache不直接操作Redis,而是通过一个缓存管理器(比如RedisCacheManager
)来干活。你得在项目里配置好Redis,让Spring知道用Redis存缓存。@CacheEvict
只是告诉Spring:"这里要清缓存",具体清Redis里的哪条记录,靠注解的参数指路。
在这代码里,cacheNames
指定了要清的缓存抽屉(MENU_ID_LIST_KEY
),key
指定了抽屉里的具体"卷宗"(用户ID)。执行时,Spring会把MENU_ID_LIST_KEY
和用户ID拼起来,比如"TestModule_rbac:menu:id_list:用户ID",然后通过缓存管理器告诉Redis删掉这条记录。
@CacheEvict
需要读方法里的啥操作?
它其实不关心方法里干了啥,也不读具体操作(比如insertUserAndUserRole
)。它只看方法跑完后要不要清缓存。清不清、清理啥,完全靠注解的配置:
- 触发时机 :默认是方法成功执行完后清缓存。如果方法抛异常(比如数据库挂了),搭配
@Transactional
,事务回滚后缓存也不会清。 - 参数提取 :
key="#userRoleDTO.userId"
用了个SpEL表达式(Spring Expression Language),从方法参数userRoleDTO
里掏出userId
。这表达式很灵活,能动态生成缓存键。
换句话说,@CacheEvict
只管"清",不管"怎么变",逻辑全在注解参数里定了。
@CacheEvict
从哪儿来?参数有啥用?
@CacheEvict
来自Spring框架的org.springframework.cache.annotation
包,是Spring Cache的核心注解之一。要用它,得在项目里加Spring Cache支持(比如加个@EnableCaching
),然后配置Redis作为缓存后端。
参数有这么几个,咱挨个儿说:
cacheNames
或value
(必填):指定缓存的"抽屉名",可以是数组,多个抽屉一起清。这里是MENU_ID_LIST_KEY
,对应"TestModule_rbac:menu:id_list:"。key
(可选):具体要清的缓存键,默认是方法签名生成的哈希值。这里用#userRoleDTO.userId
,动态取用户ID,拼成完整键。allEntries
(默认false):设成true就清整个cacheNames
里的所有缓存,不管key
。这里没用,默认只清指定键。beforeInvocation
(默认false):设成true就在方法执行前清缓存,而不是执行后。这里默认false,方法成功跑完再清。condition
(可选):加个条件,比如condition="#userRoleDTO.userId > 0"
,只有用户ID大于0才清。没用默认都清。
在这代码里,@CacheEvict(cacheNames = CacheNames.MENU_ID_LIST_KEY, key = "#userRoleDTO.userId")
意思是:方法跑完后,清掉"TestModule_rbac:menu:id_list:用户ID"这条缓存。简单直接。
为啥只清菜单ID?
角色变了,菜单ID缓存(MENU_ID_LIST_KEY
)清了,但用户权限缓存(USER_PERMISSIONS_KEY
)没动。可能是菜单跟角色关系更直接,角色一变,菜单得重新算。但权限缓存没清,得看系统咋设计的,可能有其他地方管,或者故意让它过会儿自己过期(TTL)。
3. 权限校验:Redis咋帮忙?
最后看PermissionFeignController.java
,这是权限校验的"门卫",每次请求来了,得问"你有权进吗"。
less
@RestController
public class PermissionFeignController implements PermissionFeignClient {
@Autowired
private MenuPermissionService menuPermissionService;
@Override
public ServerResponseEntity<Boolean> checkPermission(@RequestParam("userId") Long userId,
@RequestParam("sysType") Integer sysType, @RequestParam("uri") String uri,
@RequestParam("isAdmin") Integer isAdmin, @RequestParam("method") Integer method) {
List<String> userPermissions = menuPermissionService.listUserPermissions(userId, sysType, BooleanUtil.isTrue(isAdmin));
List<UriPermissionBO> uriPermissions = menuPermissionService.listUriPermissionInfo(sysType);
AntPathMatcher pathMatcher = new AntPathMatcher();
for (UriPermissionBO uriPermission : uriPermissions) {
if (pathMatcher.match(uriPermission.getUri(), uri) &&
Objects.equals(uriPermission.getMethod(), method)) {
if (userPermissions.contains(uriPermission.getPermission())) {
return ServerResponseEntity.success(Boolean.TRUE);
}
return ServerResponseEntity.fail(ResponseEnum.UNAUTHORIZED);
}
}
return ServerResponseEntity.success(Boolean.TRUE);
}
}
- 从Redis拿数据 :
listUserPermissions
和listUriPermissionInfo
大概率从Redis里捞用户权限和URI权限映射。Redis快,查起来省时间。 - 匹配URI :用
AntPathMatcher
检查请求的URI(比如"/api/order")能不能跟缓存里的URI(比如"/api/**")对上,还得看请求方法(GET、POST)对不对。 - 比对权限:URI对上了,再看用户权限里有没有这条URI需要的权限,有就放行,没就"没门儿"。
这流程全靠Redis缓存撑着,没缓存就得跑数据库查,慢得像乌龟爬。
这设计牛在哪儿?有啥坑?
牛的地方
- 快得飞起:权限校验靠Redis,内存操作比数据库快几十倍,高并发下不卡壳。
- 省资源:数据库少干活,压力小,能撑更多用户。
- 灵活匹配 :
AntPathMatcher
支持通配符,URI权限写个"/**"就能管一片儿,方便。 - 缓存管理聪明 :
@CacheEvict
动态清缓存,搭配SpEL表达式,精准打击。
可能的坑
-
用户权限缓存没清:角色变了,菜单ID缓存清了,但用户权限缓存没动。比如用户从"管理员"降到"普通用户",权限缓存没变,他还能干管理员的事儿。
- 咋整? 清权限缓存,或者设短TTL让它自己刷新。
-
缓存一致性:分布式系统里,多台服务器用Redis,数据没同步好就乱。得用消息队列或分布式锁搞定。
-
Redis内存压力:用户多、权限杂,缓存占内存多,得盯着,别爆了。
-
@CacheEvict依赖配置:没配好Redis或Spring Cache,注解就是个摆设,得检查环境。
接地气的总结
RBAC和Redis搭伙,核心是用Redis当"速查手册",把权限、菜单、URI映射记下来,省得老翻数据库。角色变了,@CacheEvict
跳出来清菜单缓存,靠Spring Cache和Redis管理器干活,不读方法逻辑,只看参数拼键。校验时Redis和AntPathMatcher
配合,速度快、效率高。不过得注意权限缓存更新和内存管理,不然快是快了,准头没了。这套路在电商、后台系统里挺常见,想用的话,把坑填平,Redis调好,缓存策略想全,就能又快又稳!