SpringBoot 中 AOP 实现权限校验(角色/权限)

做后端开发的同学都知道,权限控制是项目的重中之重:有些接口只有管理员能访问,有些接口需要特定权限才能操作,比如"删除用户""导出数据""修改配置"。

如果在每个 Controller 方法里都写 if(role != "admin") if(!hasPermission("user:delete")),不仅代码冗余、难以维护,还会让核心业务逻辑变得混乱。

而用 AOP 实现权限校验,只需一行自定义注解,就能完成接口的角色和权限控制,完全不侵入业务代码,干净、优雅、可扩展,是企业项目的标配方案。

本篇文章我们就用AOP来实现一个实际案例,实现系统权限控制。

一、权限校验的核心场景

不同于简单的角色校验,企业级权限校验需要兼顾灵活性和严谨性,本次实战覆盖以下核心需求,可直接适配大部分项目:

    1. 角色校验:某些接口仅允许指定角色访问(如 admin 管理员、manager 经理),支持多角色配置(如 roles = {"admin", "manager"});
    1. 权限码校验:某些接口需要用户拥有指定权限码才能访问(如 user:add 新增用户、user:delete 删除用户),支持多权限码配置;
    1. 校验逻辑灵活配置:支持 AND(所有条件必须满足)、OR(满足任一条件即可)两种逻辑,适配不同业务场景;
    1. 超级管理员豁免:超级管理员(如 super_admin)跳过所有权限校验,无需重复配置;
    1. 统一异常响应:无权限时返回统一的 JSON 格式,包含错误码、错误信息,便于前端统一处理;
    1. 不侵入业务代码:全程通过 AOP 增强,业务方法无需修改,降低耦合度;
    1. 适配实际项目:结合 JWT 解析用户信息(替代模拟上下文),贴合企业真实开发场景。

二、整体设计思路

权限校验的核心逻辑是"拦截接口 → 获取用户权限 → 对比校验 → 放行/拦截",用 AOP 实现的整体流程如下,步骤清晰、逻辑连贯:

    1. 自定义注解 :创建 @RequiresPermission 注解,用于标记接口需要的角色、权限码和校验逻辑;
    1. 用户上下文:结合 JWT 解析当前登录用户信息,获取用户的角色和权限列表(替代模拟数据,贴合实战);
    1. AOP 切面 :定义切点(拦截所有添加了 @RequiresPermission 注解的方法),用环绕通知实现权限校验逻辑;
    1. 校验逻辑实现:分别实现角色校验、权限码校验,支持 AND/OR 逻辑,添加超级管理员豁免机制;
    1. 全局异常处理:捕获权限校验失败的异常,返回统一的 JSON 响应,避免直接抛出异常暴露接口细节;
    1. 接口测试:覆盖正常访问、角色不足、权限不足、超级管理员豁免等场景,验证校验效果。

补充说明:AOP 切面的执行顺序很重要,权限校验需要优先于日志切面(避免无权限请求也记录日志),因此给切面添加 @Order(1) 注解(值越小,执行优先级越高)。

三、完整代码

步骤1:导入核心依赖

除了 AOP 、 JWT 依赖,无需额外导入其他包,pom.xml 如下:

go 复制代码
<!-- Spring AOP 核心依赖(权限校验核心) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

<!-- JWT 依赖(用于解析用户信息,企业实战必备) -->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<!-- 工具包(用于 JSON 响应、字符串处理,简化代码) -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson2</artifactId>
    <version>2.0.32</version>
</dependency>

步骤2:自定义权限注解

创建 @RequiresPermission 注解,用于标记接口需要的角色、权限码和校验逻辑,注解的属性设计贴合企业实际需求,支持灵活配置:

go 复制代码
import java.lang.annotation.*;

/**
 * 自定义权限校验注解
 * @Target(ElementType.METHOD):仅作用于方法(接口方法)
 * @Retention(RetentionPolicy.RUNTIME):运行时保留,AOP 切面可获取注解属性
 * @Documented:生成 API 文档时,显示该注解
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequiresPermission {

    /**
     * 需要的角色(如 "admin"、"manager"),支持多角色配置
     * 默认为空数组,表示不校验角色
     */
    String[] roles() default {};

    /**
     * 需要的权限码(如 "user:add"、"user:delete"),支持多权限码配置
     * 默认为空数组,表示不校验权限码
     */
    String[] permissions() default {};

    /**
     * 校验逻辑:AND(所有条件必须满足)、OR(满足任一条件即可)
     * 默认为 AND,即角色和权限码都满足时,才允许访问
     */
    Logical logical() default Logical.AND;

    /**
     * 是否忽略超级管理员校验(默认不忽略)
     * 若为 true,超级管理员无需满足角色和权限码,直接放行
     */
    boolean ignoreSuperAdmin() default true;
}

/**
 * 校验逻辑枚举(清晰区分 AND/OR,避免魔法值)
 */
enum Logical {
    AND,  // 必须全部满足
    OR    // 满足一个即可
}

注解属性说明:

  • • roles:可配置多个角色,如 roles = {"admin", "manager"},表示只有这两个角色能访问;

  • • permissions:可配置多个权限码,如 permissions = {"user:add", "user:edit"},表示需要拥有这些权限才能访问;

  • • logical:控制校验逻辑,比如 logical = Logical.OR 表示"角色满足 或 权限满足"即可访问;

  • • ignoreSuperAdmin:超级管理员豁免开关,开启后,超级管理员无需校验角色和权限,直接放行。

步骤3:用户上下文 + JWT 工具类

实际项目中,用户信息不会是模拟的,而是从请求头的 JWT Token 中解析获取。这里实现完整的 JWT 工具类和用户上下文,贴合企业实战:

3.1 用户实体类(存储用户核心信息)
go 复制代码
import lombok.Data;
import java.util.List;

/**
 * 用户实体类(存储当前登录用户的核心信息)
 */
@Data
public class LoginUser {
    // 用户ID
    private Long userId;
    // 用户名
    private String username;
    // 用户角色(如 "admin"、"user")
    private String role;
    // 用户拥有的权限码列表(如 ["user:list", "user:delete"])
    private List<String> permissions;
    // 是否为超级管理员(true=是,false=否)
    private boolean isSuperAdmin;
}
3.2 JWT 工具类(解析 Token、获取用户信息)
go 复制代码
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.List;

/**
 * JWT 工具类(企业实战常用,用于生成、解析 Token)
 */
@Component
public class JwtUtils {

    // 从配置文件读取 JWT 密钥(实际项目配置在 application.yml 中)
    @Value("${jwt.secret}")
    private String secret;

    // Token 过期时间(单位:毫秒,这里设置为 24 小时)
    @Value("${jwt.expiration}")
    private Long expiration;

    /**
     * 解析 Token,获取用户信息(核心方法)
     */
    public LoginUser parseToken(String token) {
        // 1. 解析 Token,获取声明信息
        Claims claims = Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();

        // 2. 从声明中提取用户信息,封装为 LoginUser 对象
        LoginUser loginUser = new LoginUser();
        loginUser.setUserId(Long.parseLong(claims.get("userId").toString()));
        loginUser.setUsername(claims.get("username").toString());
        loginUser.setRole(claims.get("role").toString());
        loginUser.setPermissions((List<String>) claims.get("permissions"));
        loginUser.setSuperAdmin(Boolean.parseBoolean(claims.get("isSuperAdmin").toString()));

        return loginUser;
    }

    /**
     * 生成 Token(可选,用于登录接口返回 Token)
     */
    public String generateToken(LoginUser loginUser) {
        Date now = new Date();
        Date expirationDate = new Date(now.getTime() + expiration);

        // 生成 Token,将用户核心信息存入声明
        return Jwts.builder()
                .setSubject(loginUser.getUsername())
                .claim("userId", loginUser.getUserId())
                .claim("role", loginUser.getRole())
                .claim("permissions", loginUser.getPermissions())
                .claim("isSuperAdmin", loginUser.isSuperAdmin())
                .setIssuedAt(now)
                .setExpiration(expirationDate)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
    }
}
3.3 配置文件(application.yml)添加 JWT 配置
go 复制代码
# JWT 配置(企业实战必备)
jwt:
  secret: springboot-aop-permission-2026 # 密钥(实际项目建议用复杂字符串,加密存储)
  expiration: 86400000 # Token 过期时间(24小时,单位:毫秒)
3.4 用户上下文(从请求头获取 Token,解析用户信息)
go 复制代码
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;

/**
 * 用户上下文(全局获取当前登录用户信息,简化代码)
 */
public class UserContext {

    // 从请求头获取 Token,解析并返回当前登录用户信息
    public static LoginUser getCurrentUser() {
        // 1. 获取当前请求对象
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        // 2. 从请求头获取 Token(请求头key:Authorization,格式:Bearer Token)
        String token = request.getHeader("Authorization");
        if (token == null || token.isEmpty()) {
            throw new RuntimeException("未登录,请先登录");
        }
        // 去除 Token 前缀 "Bearer "(前端传递时通常会加)
        if (token.startsWith("Bearer ")) {
            token = token.substring(7);
        }

        // 3. 解析 Token,返回用户信息(注入 JWT 工具类)
        JwtUtils jwtUtils = SpringContextUtils.getBean(JwtUtils.class);
        return jwtUtils.parseToken(token);
    }

    // 简化方法:获取当前用户角色
    public static String getCurrentRole() {
        return getCurrentUser().getRole();
    }

    // 简化方法:获取当前用户权限列表
    public static List<String> getCurrentPermissions() {
        return getCurrentUser().getPermissions();
    }

    // 简化方法:判断当前用户是否为超级管理员
    public static boolean isSuperAdmin() {
        return getCurrentUser().isSuperAdmin();
    }
}
3.5 补充 Spring 上下文工具类(用于在非 Spring 管理类中获取 Bean)
go 复制代码
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 * Spring 上下文工具类(用于在 UserContext 中获取 JwtUtils Bean)
 */
@Component
public class SpringContextUtils implements ApplicationContextAware {

    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextUtils.applicationContext = applicationContext;
    }

    // 根据 Bean 类型获取 Bean
    public static <T> T getBean(Class<T> clazz) {
        return applicationContext.getBean(clazz);
    }

    // 根据 Bean 名称获取 Bean(可选)
    public static Object getBean(String beanName) {
        return applicationContext.getBean(beanName);
    }
}

步骤4:AOP 权限校验切面

这是本次实战的核心,创建切面类,实现权限校验的全部逻辑:拦截注解标记的接口、获取用户信息、角色校验、权限码校验、超级管理员豁免,代码添加详细注释,便于理解和修改:

go 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * 权限校验切面(核心类)
 * @Aspect:标记此类为 AOP 切面
 * @Component:交给 Spring 管理,确保 Spring 能扫描到该切面
 * @Order(1):设置切面执行优先级,1 表示优先执行(高于日志切面,避免无权限请求记录日志)
 */
@Aspect
@Component
@Order(1)
public class PermissionAspect {

    // 1. 定义切点:拦截所有添加了 @RequiresPermission 注解的方法
    @Pointcut("@annotation(com.example.demo.annotation.RequiresPermission)")
    public void permissionPointcut() {} // 切点方法,无实际业务逻辑,仅用于标记切点

    // 2. 环绕通知:包裹目标方法,实现权限校验(可在方法执行前、执行后处理)
    @Around("permissionPointcut()")
    public Object checkPermission(ProceedingJoinPoint joinPoint) throws Throwable {
        // 第一步:获取目标方法上的 @RequiresPermission 注解
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method targetMethod = signature.getMethod();
        RequiresPermission permissionAnno = targetMethod.getAnnotation(RequiresPermission.class);

        // 若注解为空(理论上不会出现,防止异常),直接放行
        if (permissionAnno == null) {
            return joinPoint.proceed();
        }

        // 第二步:获取当前登录用户的信息(从 UserContext 中获取,贴合实战)
        LoginUser currentUser = UserContext.getCurrentUser();
        String currentRole = currentUser.getRole();
        List<String> currentPermissions = currentUser.getPermissions();
        boolean isSuperAdmin = currentUser.isSuperAdmin();

        // 第三步:超级管理员豁免校验(如果注解开启了豁免开关)
        if (permissionAnno.ignoreSuperAdmin() && isSuperAdmin) {
            // 超级管理员,直接放行,无需校验角色和权限
            return joinPoint.proceed();
        }

        // 第四步:校验角色(如果注解配置了需要的角色)
        boolean roleCheckPass = checkRole(permissionAnno, currentRole);
        if (!roleCheckPass) {
            throw new PermissionException(403, "无访问角色权限,需拥有角色:" + Arrays.toString(permissionAnno.roles()));
        }

        // 第五步:校验权限码(如果注解配置了需要的权限码)
        boolean permissionCheckPass = checkPermission(permissionAnno, currentPermissions);
        if (!permissionCheckPass) {
            throw new PermissionException(403, "无操作权限,需拥有权限码:" + Arrays.toString(permissionAnno.permissions()));
        }

        // 第六步:所有校验通过,执行目标方法(核心业务逻辑)
        return joinPoint.proceed();
    }

    /**
     * 角色校验逻辑
     * @param anno 权限注解(包含需要的角色)
     * @param currentRole 当前用户角色
     * @return 校验结果(true=通过,false=不通过)
     */
    private boolean checkRole(RequiresPermission anno, String currentRole) {
        String[] needRoles = anno.roles();
        // 若注解未配置角色,直接通过校验
        if (needRoles == null || needRoles.length == 0) {
            return true;
        }

        // 判断当前用户角色是否在需要的角色列表中
        boolean hasTargetRole = Arrays.asList(needRoles).contains(currentRole);

        // 根据校验逻辑(AND/OR)返回结果
        if (anno.logical() == Logical.OR) {
            // OR 逻辑:只要拥有一个需要的角色,就通过
            return hasTargetRole;
        } else {
            // AND 逻辑:需要拥有所有配置的角色(实际中多角色 AND 场景较少,此处兼容)
            return Arrays.stream(needRoles).allMatch(role -> role.equals(currentRole));
        }
    }

    /**
     * 权限码校验逻辑
     * @param anno 权限注解(包含需要的权限码)
     * @param currentPermissions 当前用户拥有的权限码列表
     * @return 校验结果(true=通过,false=不通过)
     */
    private boolean checkPermission(RequiresPermission anno, List<String> currentPermissions) {
        String[] needPermissions = anno.permissions();
        // 若注解未配置权限码,直接通过校验
        if (needPermissions == null || needPermissions.length == 0) {
            return true;
        }

        // 判断当前用户是否拥有需要的权限码(至少一个/全部,根据逻辑)
        if (anno.logical() == Logical.OR) {
            // OR 逻辑:拥有任意一个需要的权限码,就通过
            return Arrays.stream(needPermissions).anyMatch(currentPermissions::contains);
        } else {
            // AND 逻辑:需要拥有所有配置的权限码
            return Arrays.stream(needPermissions).allMatch(currentPermissions::contains);
        }
    }
}

步骤5:自定义权限异常(区分权限异常和其他异常)

创建自定义异常类,用于区分权限校验失败和其他业务异常,便于全局异常处理器精准捕获和返回对应信息:

go 复制代码
import lombok.Data;
import lombok.EqualsAndHashCode;

/**
 * 自定义权限异常(权限校验失败时抛出)
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class PermissionException extends RuntimeException {
    // 错误码(如 403 无权限)
    private Integer code;
    // 错误信息
    private String message;

    // 构造方法(简化异常抛出)
    public PermissionException(Integer code, String message) {
        super(message);
        this.code = code;
        this.message = message;
    }
}

步骤6:全局异常处理器(统一响应格式)

拦截权限异常和其他异常,返回统一的 JSON 格式,便于前端统一处理(如弹窗提示无权限),同时隐藏接口内部异常细节,提升安全性:

go 复制代码
import com.alibaba.fastjson2.JSONObject;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理器(统一异常响应)
 * @RestControllerAdvice:作用于所有 @RestController 注解的接口
 */
@RestControllerAdvice
public class GlobalExceptionHandler {

    // 拦截自定义权限异常(权限校验失败)
    @ExceptionHandler(PermissionException.class)
    public JSONObject handlePermissionException(PermissionException e) {
        JSONObject response = new JSONObject();
        response.put("code", e.getCode());
        response.put("msg", e.getMessage());
        response.put("data", null);
        return response;
    }

    // 拦截未登录异常(从 UserContext 中抛出)
    @ExceptionHandler(RuntimeException.class)
    public JSONObject handleRuntimeException(RuntimeException e) {
        JSONObject response = new JSONObject();
        // 区分未登录和其他运行时异常
        if (e.getMessage().contains("未登录")) {
            response.put("code", 401);
            response.put("msg", e.getMessage());
        } else {
            response.put("code", 500);
            response.put("msg", "服务器内部异常,请联系管理员");
        }
        response.put("data", null);
        return response;
    }

    // 拦截其他异常(兜底处理)
    @ExceptionHandler(Exception.class)
    public JSONObject handleException(Exception e) {
        JSONObject response = new JSONObject();
        response.put("code", 500);
        response.put("msg", "服务器内部异常,请联系管理员");
        response.put("data", null);
        return response;
    }
}

步骤7:接口使用示例

在 Controller 接口上添加 @RequiresPermission 注解,根据业务需求配置角色、权限码和校验逻辑,无需修改接口内部业务代码:

go 复制代码
import com.example.demo.annotation.RequiresPermission;
import com.example.demo.entity.LoginUser;
import com.example.demo.util.UserContext;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 测试接口(覆盖权限校验多场景)
 */
@RestController
@RequestMapping("/system")
public class SystemController {

    /**
     * 场景1:仅管理员(admin)能访问(无权限码校验)
     * 普通用户访问会被拦截,提示"无访问角色权限"
     */
    @RequiresPermission(roles = "admin")
    @GetMapping("/user/list")
    public String userList() {
        // 核心业务逻辑:查询用户列表
        return "管理员查询用户列表成功";
    }

    /**
     * 场景2:需要拥有 user:delete 权限码才能访问(无角色校验)
     * 无该权限码的用户访问会被拦截
     */
    @RequiresPermission(permissions = "user:delete")
    @PostMapping("/user/delete")
    public String deleteUser() {
        // 核心业务逻辑:删除用户
        return "删除用户成功";
    }

    /**
     * 场景3:角色为 admin 或 拥有 user:export 权限码,即可访问(OR 逻辑)
     * 满足任一条件就放行
     */
    @RequiresPermission(roles = "admin", permissions = "user:export", logical = Logical.OR)
    @GetMapping("/user/export")
    public String exportUser() {
        // 核心业务逻辑:导出用户数据
        return "导出用户数据成功";
    }

    /**
     * 场景4:角色为 admin 且 拥有 user:edit 权限码,才能访问(AND 逻辑)
     * 必须同时满足两个条件才放行
     */
    @RequiresPermission(roles = "admin", permissions = "user:edit", logical = Logical.AND)
    @PostMapping("/user/edit")
    public String editUser() {
        // 核心业务逻辑:修改用户信息
        return "修改用户信息成功";
    }

    /**
     * 场景5:超级管理员豁免校验(即使不满足角色和权限,也能访问)
     * 普通用户访问会被拦截,超级管理员直接放行
     */
    @RequiresPermission(roles = "admin", permissions = "system:config", ignoreSuperAdmin = true)
    @GetMapping("/config")
    public String getSystemConfig() {
        // 核心业务逻辑:查询系统配置
        LoginUser currentUser = UserContext.getCurrentUser();
        return currentUser.getUsername() + "查询系统配置成功";
    }
}

四、测试验证

为了确保权限校验功能正常,我们覆盖 5 种核心场景进行测试,模拟不同用户的访问情况,验证校验效果(测试工具:Postman)。

测试准备:创建 3 个测试用户

    1. 用户1:普通用户(role = "user",permissions = {"user:list", "user:query"},非超级管理员);
    1. 用户2:管理员(role = "admin",permissions = {"user:add", "user:edit"},非超级管理员);
    1. 用户3:超级管理员(role = "super_admin",permissions = {},isSuperAdmin = true)。

测试结果

访问接口 测试用户 测试结果 说明
/system/user/list 普通用户 拦截(403) 普通用户角色不是 admin,不满足角色校验
/system/user/delete 管理员 拦截(403) 管理员无 user:delete 权限码,不满足权限校验
/system/user/export 普通用户 拦截(403) 普通用户既不是 admin,也无 user:export 权限
/system/user/export 管理员 放行(200) 管理员角色满足 OR 逻辑,无需校验权限码
/system/config 超级管理员 放行(200) 超级管理员豁免校验,直接放行

测试结论:所有场景均符合预期,权限校验生效,无权限时返回统一的 403 响应,超级管理员豁免机制正常,不影响核心业务逻辑。

文末小结

SpringBoot + AOP 实现权限校验,是企业项目中最优雅、最高效的方案,核心逻辑就是「注解标记 + AOP 拦截 + 上下文校验」,全程不侵入业务代码,可扩展性极强。

如果你在实战中遇到问题(比如切面不生效、JWT 解析失败、数据权限扩展),欢迎在评论区留言交流,一起避坑、一起进步!

别忘了点赞+在看+收藏三连,关注我,解锁更多 SpringBoot 实战干货,下期再见❤️

相关推荐
掘金者阿豪2 小时前
接手一个烂摊子之后:金仓数据库开发规范实战笔记
后端
桌面运维家2 小时前
IDV云桌面vDisk机房部署方案模板特性解析
java·开发语言·devops
Stark-C2 小时前
NAS音乐必备神器,全平台音乐收割机!极空间部署『Go Music DL』
开发语言·后端·golang
哈密瓜刨冰2 小时前
深入浅出 SpringMVC:核心注解全解析与实战用法
java
Ailan_Anjuxi2 小时前
Python快速学习——第7章:选择语句
后端
用户79140679683932 小时前
分库分表策略
后端
常利兵3 小时前
大文件上传不再卡顿:Spring Boot 分片上传、断点续传与进度条实现全解析
spring boot·后端·php
用户79140679683933 小时前
MySQL的索引类型
后端
楼田莉子3 小时前
同步/异步日志系统:日志器管理器模块\全局接口\性能测试
linux·服务器·开发语言·c++·后端·设计模式