Careeroza
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) { ... }