Spring Security多级别角色权限优化:替代控制器内条件判断方案
细粒度权限控制优化方案
针对你当前在控制器内重复编写部门/团队资源校验逻辑的问题,有几种更优雅的统一处理方式,完全可以替代硬编码的条件判断:
方案一:自定义SpEL表达式函数(适配@PreAuthorize)
利用Spring Security支持的SpEL表达式扩展,将部门/团队校验逻辑封装为可复用的函数,直接在@PreAuthorize中调用。
步骤1:编写权限校验工具类
将资源归属校验逻辑统一封装,同时兼容超级管理员的全局权限:
@Component public class PermissionChecker { @Autowired private ResourceService resourceService; // 用于查询资源所属部门/团队 // 校验部门管理员是否拥有资源权限 public boolean isDeptResourceAccessible(Long resourceId) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 超级管理员直接放行 if (auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"))) { return true; } // 从自定义UserDetails中获取当前用户的部门ID CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); Long userDeptId = user.getDeptId(); // 查询资源所属部门ID并对比 Resource resource = resourceService.getById(resourceId); return resource != null && resource.getDeptId().equals(userDeptId); } // 校验团队管理员是否拥有资源权限 public boolean isTeamResourceAccessible(Long resourceId) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth.getAuthorities().contains(new SimpleGrantedAuthority("ROLE_SUPER_ADMIN"))) { return true; } CustomUserDetails user = (CustomUserDetails) auth.getPrincipal(); Long userTeamId = user.getTeamId(); Resource resource = resourceService.getById(resourceId); return resource != null && resource.getTeamId().equals(userTeamId); } }
步骤2:注册SpEL根对象
将工具类注册为SpEL的根对象,让@PreAuthorize可以直接调用其方法:
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration { @Autowired private PermissionChecker permissionChecker; @Override protected MethodSecurityExpressionHandler createExpressionHandler() { DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler(); handler.setRootObject(permissionChecker); return handler; } }
步骤3:控制器中使用
无需再写重复判断,直接在@PreAuthorize中组合角色校验和资源校验:
@RestController @RequestMapping("/resources") public class ResourceController { @GetMapping("/{id}") @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'DEPT_ADMIN') and isDeptResourceAccessible(#id)") public ResponseEntity<Resource> getDeptResource(@PathVariable Long id) { return ResponseEntity.ok(resourceService.getById(id)); } @PutMapping("/team/{id}") @PreAuthorize("hasAnyRole('SUPER_ADMIN', 'TEAM_ADMIN') and isTeamResourceAccessible(#id)") public ResponseEntity<Resource> updateTeamResource(@PathVariable Long id, @RequestBody Resource resource) { resource.setId(id); resourceService.updateById(resource); return ResponseEntity.ok(resource); } }
方案二:自定义注解+AOP(更简洁的代码风格)
通过自定义注解标记需要校验的方法,结合AOP切面统一处理权限逻辑,完全摆脱@PreAuthorize的表达式编写。
步骤1:自定义权限注解
创建针对部门、团队资源的专属注解:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckDeptResource { String resourceIdParam() default "id"; // 指定方法中资源ID的参数名 String[] allowedRoles() default {"SUPER_ADMIN", "DEPT_ADMIN"}; } @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CheckTeamResource { String resourceIdParam() default "id"; String[] allowedRoles() default {"SUPER_ADMIN", "TEAM_ADMIN"}; }
步骤2:编写AOP切面
统一处理注解标记的方法,完成角色校验和资源归属校验:
@Aspect @Component public class PermissionAspect { @Autowired private PermissionChecker permissionChecker; @Around("@annotation(checkDeptResource)") public Object checkDeptPermission(ProceedingJoinPoint joinPoint, CheckDeptResource checkDeptResource) throws Throwable { // 1. 校验角色权限 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); boolean hasValidRole = Arrays.stream(checkDeptResource.allowedRoles()) .map(role -> "ROLE_" + role) .anyMatch(role -> auth.getAuthorities().contains(new SimpleGrantedAuthority(role))); if (!hasValidRole) { throw new AccessDeniedException("无对应角色权限"); } // 2. 校验资源归属 String paramName = checkDeptResource.resourceIdParam(); MethodSignature signature = (MethodSignature) joinPoint.getSignature(); Parameter[] params = signature.getMethod().getParameters(); Long resourceId = null; for (int i = 0; i < params.length; i++) { if (params[i].getName().equals(paramName)) { resourceId = (Long) joinPoint.getArgs()[i]; break; } } if (resourceId == null || !permissionChecker.isDeptResourceAccessible(resourceId)) { throw new AccessDeniedException("无权限访问该部门资源"); } return joinPoint.proceed(); } // 同理实现CheckTeamResource的切面逻辑 }
步骤3:控制器中使用
只需添加自定义注解即可,代码更简洁:
@RestController @RequestMapping("/resources") public class ResourceController { @GetMapping("/{id}") @CheckDeptResource public ResponseEntity<Resource> getDeptResource(@PathVariable Long id) { return ResponseEntity.ok(resourceService.getById(id)); } @DeleteMapping("/team/{id}") @CheckTeamResource(resourceIdParam = "id") public ResponseEntity<Void> deleteTeamResource(@PathVariable Long id) { resourceService.removeById(id); return ResponseEntity.noContent().build(); } }
方案三:扩展PermissionEvaluator(Spring Security原生方式)
实现Spring Security的PermissionEvaluator接口,自定义权限评估逻辑,配合hasPermission()表达式使用:
@Component public class CustomPermissionEvaluator implements PermissionEvaluator { @Autowired private ResourceService resourceService; @Override public boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission) { // 处理对象级权限校验(略) return false; } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { if ("resource".equals(targetType)) { Long resourceId = (Long) targetId; if ("dept".equals(permission)) { return new PermissionChecker(resourceService).isDeptResourceAccessible(resourceId); } else if ("team".equals(permission)) { return new PermissionChecker(resourceService).isTeamResourceAccessible(resourceId); } } return false; } }
注册后在控制器中使用:
@PreAuthorize("hasPermission(#id, 'resource', 'dept')")
注意事项
- 用户信息获取:确保自定义
UserDetails实现类包含部门ID、团队ID等必要字段,从JWT解析后存入Authentication对象。 - 性能优化:对资源的部门/团队归属信息做缓存,避免每次校验都查询数据库。
- 异常处理:校验不通过时抛出
AccessDeniedException,由Spring Security统一拦截并返回权限不足的响应。
内容的提问来源于stack exchange,提问作者Coder
相关产品推荐
相关产品推荐

