Custom Authorization Beans

Moving Logic out of the Annotation

Long SpEL strings are hard to read and test. Put the logic in a normal bean and call it with @beanName:

@Component("orderSecurity")
public class OrderSecurity {

    private final OrderRepository orders;

    public OrderSecurity(OrderRepository orders) {
        this.orders = orders;
    }

    public boolean isOwner(Long orderId, Authentication authentication) {
        return orders.findById(orderId)
                .map(o -> o.getCustomer().getEmail().equals(authentication.getName()))
                .orElse(false);
    }
}

@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @PostMapping("/{orderId}/cancel")
    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)")
    public OrderDto cancel(@PathVariable Long orderId) {
        return orderService.cancel(orderId);
    }
}

Your Own Annotations

If the same rule appears on many methods, create a meta-annotation:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#orderId, authentication)")
public @interface IsOrderOwnerOrAdmin {
}

@PostMapping("/{orderId}/cancel")
@IsOrderOwnerOrAdmin
public OrderDto cancel(@PathVariable Long orderId) { ... }

Exercise

Exercise: Secure CartService.removeItem(Long cartItemId) so that only the owner of the cart item (or an admin) can call it. Use a bean named cartSecurity.

Show answer
@Component("cartSecurity")
public class CartSecurity {
    private final CartItemRepository items;
    public CartSecurity(CartItemRepository items) { this.items = items; }

    public boolean owns(Long cartItemId, Authentication auth) {
        return items.findById(cartItemId)
                .map(i -> i.getUser().getEmail().equals(auth.getName()))
                .orElse(false);
    }
}

@PreAuthorize("hasRole('ADMIN') or @cartSecurity.owns(#cartItemId, authentication)")
public void removeItem(Long cartItemId) { ... }