个人技术博客:基于 Spring AOP 与自定义注解实现 RBAC 动态权限控制

102300433袁昊 2025-12-25 19:47:15
项目信息项内容说明
这个作业属于哪个课程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 的第一步是建立支撑数据结构。为了满足高扩展性,我设计了标准的“五表模型”:

  1. 用户表 (User): 存储基础信息。
  2. 角色表 (Role): 存储角色名称(如 admin, user, auditor)。
  3. 权限表 (Permission): 存储具体行为(如 news:delete, news:audit)。
  4. 用户-角色关联表 (UserRole): 多对多关系映射。
  5. 角色-权限关联表 (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,我们可以在方法执行前“切入”一段逻辑。

实现流程如下:

  1. 定义切点:扫描所有带有 @AuthCheck 注解的方法。
  2. 获取上下文:从 RequestContextHolder 中拿到 HttpServletRequest
  3. 身份识别:解析请求头中的 Token(结合 JWT),获取当前登录用户的 ID。
  4. 权限比对:查询用户当前拥有的角色,与注解中要求的 mustRole 进行比对。
  5. 放行或拦截:匹配成功则执行 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 作为缓存中间件。

  1. 策略调整:在用户登录成功并生成 JWT Token 时,同步将该用户的角色列表(List<String>)序列化后存入 Redis,Key 设计为 auth:user:roles:{userId},设置过期时间与 Token 一致。
  2. 代码优化:修改 AuthInterceptor 中的逻辑,优先从 Redis 读取角色列表。
  • Hit(命中):直接使用,耗时 < 5ms。
  • Miss(未命中):回源查数据库,并回写 Redis。
  1. 结果:优化后,鉴权逻辑的平均耗时从 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 获取当前代理对象的方式进行自调用。

  1. 在启动类添加注解 @EnableAspectJAutoProxy(exposeProxy = true),暴露代理对象。
  2. 修改调用代码为:
public void A() {
    // 强制获取当前的代理对象进行调用
    ((NewsService) AopContext.currentProxy()).B();
}

通过这种方式,成功确保了即使是内部调用,权限校验逻辑依然能够被正确触发。

4. 总结

通过在“冰鉴 InsightNews”项目中实践 Spring AOP + 自定义注解 + RBAC 的权限控制方案,我深刻体会到了关注点分离(Separation of Concerns)的设计美学。

  1. 架构层面:我们将安全校验逻辑从具体的业务代码中完全剥离,使得 Controller 层代码更加纯粹、易读。
  2. 性能层面:结合 Redis 缓存策略,我们在保证安全性的同时,并未牺牲系统的响应速度。
  3. 扩展层面:未来如果需要增加“IP黑名单限制”或“接口限流”功能,只需复用这套 AOP 框架增加新的注解即可,无需改动现有业务代码。

这套方案不仅解决了项目中的鉴权需求,更为构建生产级的高并发系统积累了宝贵的工程经验。

5. 参考文献

  1. Spring Framework Documentation - Aspect Oriented Programming with Spring
  1. Redis 官方文档 - Redis作为缓存的使用策略
  1. MyBatis-Plus 官方文档 - 多表关联查询实现
...全文
86 回复 打赏 收藏 转发到动态 举报
写回复
用AI写文章
回复
切换为时间正序
请发表友善的回复…
发表回复

114

社区成员

发帖
与我相关
我的任务
社区描述
202501福大-软件工程实践-W班
软件工程团队开发结对编程 高校 福建省·福州市
社区管理员
  • 202501福大-软件工程实践-W班
  • 离离原上羊羊吃大草
  • MiraiZz2
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧