114
社区成员
发帖
与我相关
我的任务
分享| 项目信息项 | 内容说明 |
|---|---|
| 这个作业属于哪个课程 | 202501福大-软件工程实践-W班 |
| 这个作业的要求在哪里 | 软件工程实践总结&个人技术博客 |
| 这个作业的目标 | 总结RBAC模型在冰鉴InsightNews中的应用 |
| 其他参考文献 | 《构建之法(第三版)》 |
RBAC(Role-Based Access Control) 是一种广泛应用于企业级系统的权限管理模型。它的核心思想是不直接将权限授予用户,而是将权限授予“角色”,再将角色授予用户,从而实现用户与权限的解耦。
在“冰鉴 InsightNews”项目中,我们需要区分普通用户、管理员和内容审核员,不同角色对接口的访问权限截然不同。如果采用传统的 if-else 硬编码方式(例如 if(user.getType() != "admin") return error),会导致业务代码中充斥着大量的非业务逻辑,既难以维护又容易遗漏。
因此,我采用了 Spring AOP(面向切面编程) 配合 自定义注解 的技术方案。这种方案的难点在于:如何设计通用的切面逻辑以适配不同的接口要求,以及如何在拦截过程中结合 Redis 缓存来解决频繁查询数据库导致的性能瓶颈。
本方案的实现可以拆分为三个核心步骤:数据库模型设计、注解定义、以及AOP 切面逻辑实现。
实现 RBAC 的第一步是建立支撑数据结构。为了满足高扩展性,我设计了标准的“五表模型”:
admin, user, auditor)。news:delete, news:audit)。通过 MyBatis-Plus 进行联表查询,我们可以快速获取指定用户拥有的所有角色和权限集合。
@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 "";
}
这是整个技术方案的引擎。通过 Spring 的 AspectJ,我们可以在方法执行前“切入”一段逻辑。
实现流程如下:
@AuthCheck 注解的方法。RequestContextHolder 中拿到 HttpServletRequest。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();
}
}
在实际开发“冰鉴”系统的过程中,这套方案并非一帆风顺,我遇到了两个较为棘手的典型问题。
问题描述:
在压力测试阶段,我们发现只要开启权限校验,接口的响应时间(RT)就会增加 200ms 以上。通过查看数据库日志,发现每一个带有 @AuthCheck 的接口被调用时,AOP 切面都会触发一次“多表关联查询”来获取用户的角色信息。对于高频访问的接口(如“浏览新闻列表”),这造成了巨大的数据库压力。
解决过程:
为了解决 I/O 瓶颈,我引入了 Redis 作为缓存中间件。
List<String>)序列化后存入 Redis,Key 设计为 auth:user:roles:{userId},设置过期时间与 Token 一致。AuthInterceptor 中的逻辑,优先从 Redis 读取角色列表。问题描述:
在 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();
}
通过这种方式,成功确保了即使是内部调用,权限校验逻辑依然能够被正确触发。
通过在“冰鉴 InsightNews”项目中实践 Spring AOP + 自定义注解 + RBAC 的权限控制方案,我深刻体会到了关注点分离(Separation of Concerns)的设计美学。
Controller 层代码更加纯粹、易读。这套方案不仅解决了项目中的鉴权需求,更为构建生产级的高并发系统积累了宝贵的工程经验。