You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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')")

注意事项

  1. 用户信息获取:确保自定义UserDetails实现类包含部门ID、团队ID等必要字段,从JWT解析后存入Authentication对象。
  2. 性能优化:对资源的部门/团队归属信息做缓存,避免每次校验都查询数据库。
  3. 异常处理:校验不通过时抛出AccessDeniedException,由Spring Security统一拦截并返回权限不足的响应。

内容的提问来源于stack exchange,提问作者Coder

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 04:30:03