AOP 自定义注解权限控制实操方法(Spring Boot 接口鉴权实战)

权限控制落到方法级,最直接的做法是自定义注解 + Spring AOP。定义 @RequirePermission 注解声明权限码,切面在 @Around 里从请求上下文解析当前用户权限集合,与注解要求的权限比对,通过则 joinPoint.proceed() 放行,否则抛业务异常返回 403。业务代码零侵入,加一个注解就是加一处权限。

一、为什么选注解 + AOP,而不是拦截器

Servlet Filter、HandlerInterceptor、Spring AOP 三层都能做鉴权,拦截点不同。

方案 作用层级 粒度 业务侵入 适合场景
Filter Servlet 容器 URL 级 低 登录态、全局过滤
Interceptor Spring MVC URL / Handler 中 登录校验、统一 header
AOP + 注解 业务方法 方法级 极低 精细权限、日志、事务

文章创作器项目里,文章创建、编辑、删除、调用 AI 生成这些接口的权限各不相同,URL 级配置写起来冗长且容易漏。注解直接标注在方法上,权限码跟方法声明在一起,谁有权限一目了然。

二、定义权限注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {

    String value();                // 权限码,如 article:create
    String[] roles() default {};   // 角色要求,空表示不限制
}

@Retention(RUNTIME) 保证注解在运行时能通过反射读到,这是切面取权限配置的前提。@Target(METHOD) 限定只能标在方法上。

三、切面实现

@Around 把权限校验和目标方法执行包在一起,先校验后放行:

@Aspect
@Component
public class PermissionAspect {

    private final PermissionService permissionService;

    @Around("@annotation(requirePermission)")
    public Object check(ProceedingJoinPoint joinPoint,
                        RequirePermission requirePermission) throws Throwable {
        // 从请求上下文取当前用户
        HttpServletRequest request =
            ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Long userId = JwtUtil.parseUserId(request.getHeader("Authorization"));

        // 取权限集合(带缓存)
        Set<String> perms = permissionService.getPermissionsByUser(userId);
        if (!perms.contains(requirePermission.value())) {
            throw new BusinessException(403, "无权限访问:" + requirePermission.value());
        }
        // 角色校验(可选)
        for (String role : requirePermission.roles()) {
            if (!permissionService.getRolesByUser(userId).contains(role)) {
                throw new BusinessException(403, "角色不符");
            }
        }
        return joinPoint.proceed();
    }
}

两个细节值得提。权限集合不能每次请求都查库,用户权限写入 Redis 缓存,修改角色后清缓存,鉴权耗时压到毫秒级。@Around(“@annotation(requirePermission)”) 这种写法把注解实例直接绑进方法参数,省掉反射 getAnnotation 的步骤。

四、在接口上使用

@PostMapping("/article")
@RequirePermission("article:create")
public Result<ArticleVO> create(@RequestBody ArticleCreateDTO dto) {
    return articleService.create(dto);
}

@DeleteMapping("/article/{id}")
@RequirePermission("article:delete")
public Result<Void> delete(@PathVariable Long id) {
    return articleService.delete(id);
}

权限码存库,通过用户-角色-权限三级关联查到,这里只做”声明 + 校验”两层。

五、落地步骤

  1. 引入 spring-boot-starter-aop 依赖,Spring Boot 自动装配切面,无需额外配置;
  2. 建用户、角色、权限三张表,用户角色、角色权限用关联表维护;
  3. 定义注解,方法上标注权限码;
  4. 写切面类,解析 Token 取用户,比对权限集合;
  5. 全局异常处理器捕获 BusinessException,统一返回 403 响应结构;
  6. 联调验证,权限不足的接口返回 403 且不执行业务代码。

六、注解放在 Controller 还是 Service

两类位置都能用,效果不同。Controller 层鉴权贴近接口语义,权限码跟 REST 资源一一对应,前端报错定位直观,适合文章创建、删除这类对外接口。Service 层鉴权贴近业务,同一方法被多个接口调用时只写一次校验,适合 AI 生成这类内部也会被复用的核心方法。

位置 优点 缺点 适用
Controller 方法 权限码与接口资源一一对应,语义清晰 内部复用时不生效 对外 REST 接口
Service 方法 一处校验多处生效,覆盖面全 权限码与接口解耦,报错定位略绕 被复用的核心逻辑

项目里把「对外写操作」放 Controller,把「AI 生成、配额扣减」放 Service,两层配合覆盖完整。

七、三个易踩的坑

自调用失效。同一个类内部方法互相调用,AOP 代理不生效,权限校验被跳过。解决方式是拆到不同类,或通过 AopContext.currentProxy() 拿代理再调。判断是否生效,debug 看注入的 bean 是不是 CGLIB 代理类。

切面顺序。多个切面(鉴权、日志、限流)并存时用 @Order 控制执行顺序,鉴权切面优先执行,先拦截非法请求再记日志,避免日志里混进一堆被拒请求。

切面里抛异常要走业务异常,不要 return null 或吞掉。proceed() 之外的分支异常要被全局异常处理器接住,前端才能拿到统一响应结构可靠处理。

常见问题(FAQ)

Q1:AOP 权限校验在拦截器前面还是后面?

AOP 在 Controller 方法调用时生效,早于业务代码,晚于拦截器。

Q2:注解权限和 Spring Security 怎么配合?

可并存。Spring Security 管登录态,注解切面管细粒度权限码。

Q3:方法内部自调用会跳过鉴权吗?

会。同类内部调用不走代理,需拆类或手动取代理对象。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
小码农的头像小码农认证作者

相关推荐

返回顶部