package com.shop.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
@Bean
MethodSecurityExpressionHandler methodSecurityExpressionHandler(PermissionEvaluator permissionEvaluator) {
DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
}
package com.shop.security;
import com.shop.cart.Cart;
import com.shop.cart.CartRepository;
import java.io.Serializable;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
@Component
public class CartPermissionEvaluator implements PermissionEvaluator {
private final CartRepository carts;
public CartPermissionEvaluator(CartRepository carts) {
this.carts = carts;
}
@Override
public boolean hasPermission(Authentication auth, Serializable targetId,
String targetType, Object permission) {
if (auth == null || targetId == null || !"CART".equals(targetType)) {
return false;
}
return carts.findById((Long) targetId)
.map(cart -> isOwner(auth, cart) && supports(cart, permission))
.orElse(false);
}
@Override
public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
throw new UnsupportedOperationException("Use id-and-type based hasPermission");
}
private boolean isOwner(Authentication auth, Cart cart) {
return cart.getOwnerUsername().equals(auth.getName());
}
private boolean supports(Cart cart, Object permission) {
if (!"CHECKOUT".equals(permission)) {
return false;
}
return !cart.isEmpty() && !cart.isCheckedOut();
}
}
package com.shop.checkout;
import com.shop.cart.CheckoutService;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/carts")
public class CheckoutController {
private final CheckoutService checkoutService;
public CheckoutController(CheckoutService checkoutService) {
this.checkoutService = checkoutService;
}
@PostMapping("/{cartId}/checkout")
@PreAuthorize("hasPermission(#cartId, 'CART', 'CHECKOUT')")
public ResponseEntity<OrderResponse> checkout(@PathVariable Long cartId) {
OrderResponse order = checkoutService.checkout(cartId);
return ResponseEntity.ok(order);
}
}
Spring Security's method-level annotations let authorization live next to the code it protects instead of being scattered across URL matchers. In MethodSecurityConfig, @EnableMethodSecurity(prePostEnabled = true) turns on the @PreAuthorize/@PostAuthorize machinery, and a MethodSecurityExpressionHandler bean wires in a custom PermissionEvaluator. Registering the handler as a bean is important: the SpEL hasPermission(...) function is inert until an evaluator is attached, so without this step the expression silently fails closed.
The real logic lives in CartPermissionEvaluator, which implements PermissionEvaluator. Spring calls hasPermission(auth, targetId, targetType, permission) — the four-argument overload — when an expression references a domain object by id and type rather than by instance. Here the evaluator loads the Cart by id, then checks two things: that the authenticated principal actually owns the cart, and that the requested permission (a string like "CHECKOUT") is satisfied. Ownership is the classic gap that role checks miss — a user with the CUSTOMER role must still only be able to check out their own cart, which is a per-object decision that no hasRole call can express. The instance-based overload is left unsupported to keep the contract narrow and avoid accidentally trusting a detached object.
CheckoutController shows the payoff. The @PreAuthorize("hasPermission(#cartId, 'CART', 'CHECKOUT')") on checkout runs before the method body, so an unauthorized request never reaches the service and never begins a transaction. The #cartId reference pulls the argument straight from the method signature, and the string literals map to the targetType and permission arguments the evaluator receives.
The trade-off is that this evaluator issues a database read on every guarded call, so it is worth caching or scoping to cheap lookups for hot paths. A subtle pitfall is exception handling: throwing a plain runtime error from the evaluator surfaces as a 500 rather than a clean 403, so CartPermissionEvaluator returns false for missing carts and lets Spring translate the denial into an AccessDeniedException. This pattern fits any endpoint where authorization depends on data ownership rather than static roles, keeping controllers declarative while centralizing the messy rules in one testable evaluator.
Related snips
type User {
id: ID!
name: String!
email: String!
posts: [Post!]!
createdAt: String!
GraphQL API with Spring Boot
package com.example.starter.config;
import com.example.starter.properties.CustomProperties;
import com.example.starter.service.CustomService;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
Custom Spring Boot starters
<%# private stream: turbo signs the serialized record name %>
<%= turbo_stream_from current_user %>
<section class="notifications">
<h1>Notifications</h1>
Turbo Streams + authorization: signed per-user stream name
package com.example.demo.config;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.apache.kafka.common.serialization.StringSerializer;
Messaging with Apache Kafka
class Document < ApplicationRecord
belongs_to :owner, class_name: "User"
has_many :visibilities, class_name: "DocumentVisibility", dependent: :delete_all
scope :public_documents, -> { where(is_public: true) }
Polymorphic “Visible To” Scope with Arel
package com.example.demo.controller;
import com.example.demo.dto.FileMetadata;
import com.example.demo.service.FileStorageService;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
File upload and download handling
Share this code
Here's the card — post it anywhere.