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

如何正确实现用户仅能操作自身关联订单的权限校验?

解决方案:订单操作的权限校验实现

当前接口仅完成了角色校验,未校验用户与订单的归属关系或操作权限,导致普通用户可修改他人订单。以下是几种易维护、可扩展的正确实现方式:

1. 扩展Spring Security方法级安全表达式

通过自定义权限校验组件,在@PreAuthorize中结合角色与订单归属判断,将权限逻辑与控制器解耦:

步骤1:定义权限校验组件

创建专门的权限校验类,封装订单操作的权限判断逻辑:

@Component("orderSecurity")
public class OrderSecurity {
    @Autowired
    private OrderService orderService;

    public boolean hasPermissionToModify(int orderId) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUserId = auth.getName(); // 假设用户ID存储在Authentication的name字段,可根据实际从Principal获取

        // 管理员直接放行
        if (auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            return true;
        }

        // 普通用户校验订单归属
        Order order = orderService.findById(orderId)
                .orElseThrow(() -> new OrderNotFoundException(orderId));
        return currentUserId.equals(order.getUserId()); // 假设Order实体包含userId字段
    }
}

步骤2:修改接口注解

在原接口的@PreAuthorize中调用自定义校验方法:

@PreAuthorize("@orderSecurity.hasPermissionToModify(#id)")
@PatchMapping(path = "/setFree", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<OrderDTO> setFree(@RequestParam(name = "id") int id) {
    final var order = this.orderService.findById(id).orElseThrow(() -> new OrderNotFoundException(id));
    if (order.getStatus() == OrderStatus.FREE.getId())
        return ResponseEntity.badRequest().build();

    final var free = this.orderService.setFree(order);
    return ResponseEntity.ok(this.modelMapper.map(free, OrderDTO.class));
}

这种方式便于后续扩展复杂规则(如支持客服角色操作特定订单),权限逻辑集中管理,更易维护。

2. 业务层封装权限校验

将权限判断下沉到业务层的核心方法中,确保所有修改订单状态的操作都经过校验:

@Service
public class OrderService {
    @Autowired
    private OrderRepository orderRepository;

    public Order setFree(Order order) {
        // 先执行权限校验
        validateOrderModificationPermission(order.getId());
        
        order.setStatus(OrderStatus.FREE.getId());
        return orderRepository.save(order);
    }

    private void validateOrderModificationPermission(int orderId) {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUserId = auth.getName();

        if (auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            return;
        }

        Order order = findById(orderId)
                .orElseThrow(() -> new OrderNotFoundException(orderId));
        if (!currentUserId.equals(order.getUserId())) {
            throw new AccessDeniedException("无权限修改该订单");
        }
    }
}

此方式的优势是,无论通过哪个入口调用订单修改方法,都会触发权限校验,避免遗漏。若存在多个订单操作接口,可减少重复代码。

3. 自定义权限注解+切面

如果项目中有大量类似的对象权限校验需求,可通过自定义注解结合AOP切面统一处理:

步骤1:自定义权限注解

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckOrderPermission {
    String orderIdParam() default "id"; // 指定订单ID参数的名称
}

步骤2:实现切面逻辑

@Aspect
@Component
public class OrderPermissionAspect {
    @Autowired
    private OrderService orderService;

    @Around("@annotation(checkOrderPermission)")
    public Object checkPermission(ProceedingJoinPoint joinPoint, CheckOrderPermission checkOrderPermission) throws Throwable {
        // 获取订单ID参数
        String paramName = checkOrderPermission.orderIdParam();
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Parameter[] parameters = signature.getMethod().getParameters();
        int orderId = 0;
        for (int i = 0; i < parameters.length; i++) {
            if (parameters[i].getName().equals(paramName)) {
                orderId = (int) joinPoint.getArgs()[i];
                break;
            }
        }

        // 执行权限校验
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        String currentUserId = auth.getName();

        if (!auth.getAuthorities().stream().anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
            Order order = orderService.findById(orderId)
                    .orElseThrow(() -> new OrderNotFoundException(orderId));
            if (!currentUserId.equals(order.getUserId())) {
                throw new AccessDeniedException("无权限修改该订单");
            }
        }

        // 校验通过,执行原方法
        return joinPoint.proceed();
    }
}

步骤3:接口上使用注解

@PreAuthorize("hasRole('USER') || hasRole('ADMIN')")
@CheckOrderPermission(orderIdParam = "id")
@PatchMapping(path = "/setFree", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<OrderDTO> setFree(@RequestParam(name = "id") int id) {
    // 原方法逻辑保持不变
}

这种方式适合批量复用权限校验逻辑,减少重复代码,同时保持控制器代码简洁。


为什么不推荐直接在控制器用SecurityHolder对比?

直接在控制器中写SecurityContextHolder.getContext().getAuthentication().getName().equals(order.getUserId())虽能快速实现,但存在明显缺陷:

  • 权限逻辑与控制器耦合,违反单一职责原则
  • 多接口场景下会产生大量重复代码
  • 复杂权限规则(如多角色、特殊权限)难以扩展和维护

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.09 16:34:56