news 2026/4/17 14:12:45

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot 中 AOP 实现权限校验(角色/权限)

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

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

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

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

一、权限校验的核心场景

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

  1. 1.角色校验:某些接口仅允许指定角色访问(如 admin 管理员、manager 经理),支持多角色配置(如 roles = {"admin", "manager"});

  2. 2.权限码校验:某些接口需要用户拥有指定权限码才能访问(如 user:add 新增用户、user:delete 删除用户),支持多权限码配置;

  3. 3.校验逻辑灵活配置:支持 AND(所有条件必须满足)、OR(满足任一条件即可)两种逻辑,适配不同业务场景;

  4. 4.超级管理员豁免:超级管理员(如 super_admin)跳过所有权限校验,无需重复配置;

  5. 5.统一异常响应:无权限时返回统一的 JSON 格式,包含错误码、错误信息,便于前端统一处理;

  6. 6.不侵入业务代码:全程通过 AOP 增强,业务方法无需修改,降低耦合度;

  7. 7.适配实际项目:结合 JWT 解析用户信息(替代模拟上下文),贴合企业真实开发场景。

二、整体设计思路

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

  1. 1.自定义注解:创建@RequiresPermission注解,用于标记接口需要的角色、权限码和校验逻辑;

  2. 2.用户上下文:结合 JWT 解析当前登录用户信息,获取用户的角色和权限列表(替代模拟数据,贴合实战);

  3. 3.AOP 切面:定义切点(拦截所有添加了@RequiresPermission注解的方法),用环绕通知实现权限校验逻辑;

  4. 4.校验逻辑实现:分别实现角色校验、权限码校验,支持 AND/OR 逻辑,添加超级管理员豁免机制;

  5. 5.全局异常处理:捕获权限校验失败的异常,返回统一的 JSON 响应,避免直接抛出异常暴露接口细节;

  6. 6.接口测试:覆盖正常访问、角色不足、权限不足、超级管理员豁免等场景,验证校验效果。

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

三、完整代码

步骤1:导入核心依赖

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

<!-- 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注解,用于标记接口需要的角色、权限码和校验逻辑,注解的属性设计贴合企业实际需求,支持灵活配置:

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 用户实体类(存储用户核心信息)
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、获取用户信息)
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 配置
# JWT 配置(企业实战必备) jwt: secret: springboot-aop-permission-2026 # 密钥(实际项目建议用复杂字符串,加密存储) expiration: 86400000 # Token 过期时间(24小时,单位:毫秒)
3.4 用户上下文(从请求头获取 Token,解析用户信息)
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)
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 权限校验切面

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

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:自定义权限异常(区分权限异常和其他异常)

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

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 格式,便于前端统一处理(如弹窗提示无权限),同时隐藏接口内部异常细节,提升安全性:

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注解,根据业务需求配置角色、权限码和校验逻辑,无需修改接口内部业务代码:

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. 用户1:普通用户(role = "user",permissions = {"user:list", "user:query"},非超级管理员);

  2. 2. 用户2:管理员(role = "admin",permissions = {"user:add", "user:edit"},非超级管理员);

  3. 3. 用户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 实战干货,下期再见❤️

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/17 14:10:59

告别命令行焦虑:在iTerm2中实现文件拖拽式上传与下载

1. 为什么我们需要更友好的文件传输方式 刚接触Mac终端的新手&#xff0c;特别是从Windows或Linux转过来的用户&#xff0c;常常会对命令行操作感到不适应。在Windows上&#xff0c;我们习惯了用Xshell这类工具直接拖拽文件上传下载&#xff0c;而到了Mac的iTerm2中&#xff0c…

作者头像 李华
网站建设 2026/4/17 14:10:54

点云去噪踩坑实录:从理论到实践,我的五个血泪教训

点云去噪踩坑实录&#xff1a;从理论到实践&#xff0c;我的五个血泪教训 去年参与一个室内场景三维重建项目时&#xff0c;我花了整整两周时间与点云去噪"搏斗"。本以为掌握了PCL和CloudCompare的基本操作就能轻松应对&#xff0c;结果却在各种算法参数和性能瓶颈中…

作者头像 李华
网站建设 2026/4/17 14:06:40

从挂科边缘到90+:华科矩阵论期末自救指南(附学长GitHub笔记)

从挂科边缘到90&#xff1a;华科矩阵论期末自救指南 凌晨三点的华科图书馆&#xff0c;总能看到几个对着矩阵论教材抓耳挠腮的身影。去年此时的我也是其中一员——距离考试只剩72小时&#xff0c;课本上那些奇异值分解、Jordan标准形的公式依然像天书般陌生。但最终&#xff0c…

作者头像 李华
网站建设 2026/4/17 14:06:39

PLD分类全解析:从低密度到高密度,从结构到工艺

1. PLD基础概念与分类逻辑 第一次接触PLD&#xff08;可编程逻辑器件&#xff09;时&#xff0c;很多人会被各种缩写搞晕。简单来说&#xff0c;PLD就像电子界的"乐高积木"——厂家提供基础模块&#xff0c;工程师通过编程自由组合这些模块来实现特定功能。这种灵活性…

作者头像 李华
网站建设 2026/4/17 14:06:23

Path of Building终极指南:3步掌握流放之路离线构建规划器

Path of Building终极指南&#xff1a;3步掌握流放之路离线构建规划器 【免费下载链接】PathOfBuilding Offline build planner for Path of Exile. 项目地址: https://gitcode.com/GitHub_Trending/pa/PathOfBuilding Path of Building&#xff08;简称PoB&#xff09;…

作者头像 李华