个人技术博客:基于 Spring AOP 与自定义注解实现 RBAC 动态权限控制
| 项目信息项 | 内容说明 |
|---|---|
| 这个作业属于哪个课程 | 202501福大-软件工程实践-W班 |
| 这个作业的要求在哪里 | 软件工程实践总结&个人技术博客 |
| 这个作业的目标 | 总结RBAC模型在冰鉴InsightNews中的应用 |
| 其他参考文献 | 《构建之法(第三版)》 |
目录
- 1. 技术概述
- 2. 技术详述
- 2.1 核心数据库模型设计
- 2.2 自定义注解 @AuthCheck
- 2.3 AOP 切面逻辑实现
- 3. 技术使用中遇到的问题与解决过程
- 问题一:频繁查库导致的性能瓶颈
- 问题二:AOP “自调用” 失效陷阱
- 4. 总结
- 5. 参考文献
1. 技术概述
RBAC(Role-Based Access Control) 是一种广泛应用于企业级系统的权限管理模型。它的核心思想是不直接将权限授予用户,而是将权限授予“角色”,再将角色授予用户,从而实现用户与权限的解耦。
在“冰鉴 InsightNews”项目中,我们需要区分普通用户、管理员和内容审核员,不同角色对接口的访问权限截然不同。如果采用传统的 if-else 硬编码方式(例如 if(user.getType() != "admin") return error),会导致业务代码中充斥着大量的非业务逻辑,既难以维护又容易遗漏。
因此,我采用了 Spring AOP(面向切面编程) 配合 自定义注解 的技术方案。这种方案的难点在于:如何设计通用的切面逻辑以适配不同的接口要求,以及如何在拦截过程中结合 Redis 缓存来解决频繁查询数据库导致的性能瓶颈。
2. 技术详述
本方案的实现可以拆分为三个核心步骤:数据库模型设计、注解定义、以及AOP 切面逻辑实现。
2.1 核心数据库模型设计
实现 RBAC 的第一步是建立支撑数据结构。为了满足高扩展性,我设计了标准的“五表模型”:
- 用户表 (User): 存储基础信息。
- 角色表 (Role): 存储角色名称(如
admin,user,auditor)。 - 权限表 (Permission): 存储具体行为(如
news:delete,news:audit)。 - 用户-角色关联表 (UserRole): 多对多关系映射。
- 角色-权限关联表 (RolePermission): 多对多关系映射。
通过 MyBatis-Plus 进行联表查询,我们可以快速获取指定用户拥有的所有角色和权限集合。
2.2 自定义注解 @AuthCheck
为了让权限控制像贴标签一样简单,我们需要定义一个元注解。
package com.insight.news.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 权限校验注解
* 加在 Controller 方法上,用于拦截未授权请求
*/
@Target(ElementType.METHOD) // 作用于方法
@Retention(RetentionPolicy.RUNTIME) // 运行时有效
public @interface AuthCheck {
/**
* 必须具备的角色,默认为空
*/
String mustRole() default "";
}
2.3 AOP 切面逻辑实现
这是整个技术方案的引擎。通过 Spring 的 AspectJ,我们可以在方法执行前“切入”一段逻辑。
实现流程如下:
- 定义切点:扫描所有带有
@AuthCheck注解的方法。 - 获取上下文:从
RequestContextHolder中拿到HttpServletRequest。 - 身份识别:解析请求头中的 Token(结合 JWT),获取当前登录用户的 ID。
- 权限比对:查询用户当前拥有的角色,与注解中要求的
mustRole进行比对。 - 放行或拦截:匹配成功则执行
joinPoint.proceed(),否则抛出自定义业务异常。
核心代码实现:
@Aspect
@Component
@Slf4j
public class AuthInterceptor {
@Resource
private UserService userService;
/**
* 执行拦截
* @param joinPoint 切入点
* @param authCheck 注解对象
*/
@Around("@annotation(authCheck)")
public Object doInterceptor(ProceedingJoinPoint joinPoint, AuthCheck authCheck) throws Throwable {
String mustRole = authCheck.mustRole();
// 1. 获取请求属性
RequestAttributes requestAttributes = RequestContextHolder.currentRequestAttributes();
HttpServletRequest request = ((ServletRequestAttributes) requestAttributes).getRequest();
// 2. 获取当前登录用户 (内部封装了 Session/JWT 解析逻辑)
User loginUser = userService.getLoginUser(request);
// 3. 校验注解中是否定义了角色要求
if (StringUtils.isNotBlank(mustRole)) {
// 获取用户实际角色列表 (从数据库或缓存)
List<String> userRoles = loginUser.getRoles();
// 4. 核心鉴权逻辑
if (userRoles == null || !userRoles.contains(mustRole)) {
log.warn("用户 {} 试图访问受限接口,所需角色: {}", loginUser.getId(), mustRole);
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "无权限访问");
}
}
// 5. 权限通过,放行执行原业务方法
return joinPoint.proceed();
}
}
3. 技术使用中遇到的问题与解决过程
在实际开发“冰鉴”系统的过程中,这套方案并非一帆风顺,我遇到了两个较为棘手的典型问题。
问题一:频繁查库导致的性能瓶颈
问题描述:
在压力测试阶段,我们发现只要开启权限校验,接口的响应时间(RT)就会增加 200ms 以上。通过查看数据库日志,发现每一个带有 @AuthCheck 的接口被调用时,AOP 切面都会触发一次“多表关联查询”来获取用户的角色信息。对于高频访问的接口(如“浏览新闻列表”),这造成了巨大的数据库压力。
解决过程:
为了解决 I/O 瓶颈,我引入了 Redis 作为缓存中间件。
- 策略调整:在用户登录成功并生成 JWT Token 时,同步将该用户的角色列表(
List<String>)序列化后存入 Redis,Key 设计为auth:user:roles:{userId},设置过期时间与 Token 一致。 - 代码优化:修改
AuthInterceptor中的逻辑,优先从 Redis 读取角色列表。
- Hit(命中):直接使用,耗时 < 5ms。
- Miss(未命中):回源查数据库,并回写 Redis。
- 结果:优化后,鉴权逻辑的平均耗时从 200ms 降低至 8ms,QPS 提升了约 10 倍。
问题二:AOP “自调用” 失效陷阱
问题描述:
在 NewsService 类中,我有一个普通方法 A() 调用了同类中的另一个带有 @AuthCheck 注解的方法 B()。
public void A() {
this.B(); // 直接调用内部方法
}
@AuthCheck(mustRole = "admin")
public void B() { ... }
测试发现,外部直接调用 B() 时鉴权生效,但通过调用 A() 间接触发 B() 时,鉴权直接被绕过了,普通用户也能执行管理员操作。
解决过程:
经过查阅 Spring 官方文档,我了解到这是 Spring AOP 的代理机制 导致的。Spring AOP 是基于代理(Proxy)实现的,当外部调用 Bean 时,实际上调用的是代理对象,代理对象会执行切面逻辑。但当 Bean 内部发生 this.B() 调用时,使用的是真实对象本身(Target),因此绕过了代理,导致切面失效。
解决方案:
我采用了 AopContext 获取当前代理对象的方式进行自调用。
- 在启动类添加注解
@EnableAspectJAutoProxy(exposeProxy = true),暴露代理对象。 - 修改调用代码为:
public void A() {
// 强制获取当前的代理对象进行调用
((NewsService) AopContext.currentProxy()).B();
}
通过这种方式,成功确保了即使是内部调用,权限校验逻辑依然能够被正确触发。
4. 总结
通过在“冰鉴 InsightNews”项目中实践 Spring AOP + 自定义注解 + RBAC 的权限控制方案,我深刻体会到了关注点分离(Separation of Concerns)的设计美学。
- 架构层面:我们将安全校验逻辑从具体的业务代码中完全剥离,使得
Controller层代码更加纯粹、易读。 - 性能层面:结合 Redis 缓存策略,我们在保证安全性的同时,并未牺牲系统的响应速度。
- 扩展层面:未来如果需要增加“IP黑名单限制”或“接口限流”功能,只需复用这套 AOP 框架增加新的注解即可,无需改动现有业务代码。
这套方案不仅解决了项目中的鉴权需求,更为构建生产级的高并发系统积累了宝贵的工程经验。
5. 参考文献
- Spring Framework Documentation - Aspect Oriented Programming with Spring
- Redis 官方文档 - Redis作为缓存的使用策略
- MyBatis-Plus 官方文档 - 多表关联查询实现
相关推荐
AOP下的权限控制实现
AOP下的权限控制实现 AOP下的权限控制实现 AOP下的权限控制实现 AOP下的权限控制实现 AOP下的权限控制实现
SpringBoot开发小而美的个人博客
SpringBoot开发小而美的个人博客
SpringBoot 权限控制(菜单控制,页面元素控制,url控制)
基于RBAC思想的权限控制,可先建立完整的库,也可以使用使用代码生成,包中提供两种方式, 代码先后顺序 菜单控制----》元素控制-------》url控制
java 权限控制轻量版
java 权限控制 轻量版
spring aop权限小实例
NULL 博文链接:https://panmingzhi0815.iteye.com/blog/1139504


