Authorization
Authorization happens after authentication. Authentication says "this is John." Authorization asks "can John do this?" It's like having an ID check AND a keycard system โ the ID gets you in the building, but only certain keycards open certain doors.
Spring Security gives you multiple ways to control access: URL-based security, role-based checks, and method-level security with annotations. Mix and match them based on your needs.
Role-Based Access Control
Roles are the most common way to control access. You assign users roles like ADMIN, USER, or MODERATOR, then restrict endpoints based on those roles. It's simple, effective, and scales well.
You can also use more fine-grained authorities if roles aren't enough. Authorities let you define specific permissions like "read:users" or "write:orders" for ultimate flexibility.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/users/**").hasAnyRole("USER", "ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/super-admin/**").hasAuthority("ROLE_SUPER_ADMIN")
.anyRequest().authenticated()
);
return http.build();
}
@Service
public class User {
private String username;
private String password;
private String role;
private List<String> permissions;
}
@PreAuthorize Annotation
@PreAuthorize lets you add security checks directly to your methods. It's like putting a guard at the door of each individual room instead of just the building entrance. More granular control, less configuration in one place.
You can use SpEL expressions to create complex authorization rules. Check roles, ownership, or even database values to decide if a user can execute a method.
@Service
public class OrderService {
@PreAuthorize("hasRole('ADMIN')")
public List<Order> getAllOrders() {
return orderRepository.findAll();
}
@PreAuthorize("#userId == authentication.principal.id or hasRole('ADMIN')")
public Order getUserOrder(Long userId, Long orderId) {
return orderRepository.findByIdAndUserId(orderId, userId)
.orElseThrow(() -> new ResourceNotFoundException("Order", "id", orderId));
}
@PreAuthorize("hasRole('USER') and @orderSecurity.isOwner(#orderId, authentication)")
public Order getOrder(Long orderId) {
return orderRepository.findById(orderId)
.orElseThrow(() -> new ResourceNotFoundException("Order", "id", orderId));
}
}
URL-Based Security
URL-based security is your first line of defense. You configure which URLs require which roles in the SecurityFilterChain. It's fast, centralized, and handles most use cases.
Combine URL-based security with method-level security for defense in depth. URL security catches the big stuff, while method-level security handles the edge cases. Together, they make your app very hard to break into.
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/posts/**").hasRole("USER")
.requestMatchers(HttpMethod.PUT, "/api/posts/**").hasRole("USER")
.requestMatchers(HttpMethod.DELETE, "/api/posts/**").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
);
return http.build();
}