Skip to main content

Spring Security Authorization

Introduction

Spring Security Authorization is a crucial aspect of application security that controls what authenticated users can access and do within your application. While authentication verifies who the user is, authorization determines what they're allowed to do.

In this tutorial, we'll explore how Spring Security implements authorization mechanisms, allowing you to define fine-grained access control rules for your Spring applications.

Understanding Authorization in Spring Security

Authorization in Spring Security is built around a few key concepts:

  1. Authorities - Simple permissions granted to users (e.g., "READ", "WRITE")
  2. Roles - Groups of authorities (e.g., "ADMIN", "USER")
  3. Access Decision Managers - Components that decide if access is granted
  4. Security Expressions - Expressions that define access rules

Let's look at each of these components in more detail.

Setting Up Basic Authorization

First, let's set up a basic Spring Security configuration with authorization:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/public/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.formLogin(withDefaults());

return http.build();
}

@Bean
public InMemoryUserDetailsManager userDetailsManager() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();

UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("USER", "ADMIN")
.build();

return new InMemoryUserDetailsManager(user, admin);
}
}

In this configuration:

  • URLs starting with /public/ are accessible to everyone
  • URLs starting with /admin/ require the "ADMIN" role
  • URLs starting with /user/ require the "USER" role
  • All other URLs require authentication
note

Using User.withDefaultPasswordEncoder() is not recommended for production. It's shown here for simplicity; in a real application, you should use a proper password encoder like BCryptPasswordEncoder.

Understanding URL-based Authorization

Spring Security lets you define access rules based on URL patterns. The order of rules matters, with more specific patterns defined first:

java
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/v1/posts/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/v1/posts").hasRole("AUTHOR")
.anyRequest().authenticated()
);

This configuration:

  1. Restricts /api/v1/admin/** URLs to users with the "ADMIN" role
  2. Allows public GET access to /api/v1/posts/**
  3. Requires the "AUTHOR" role for POST requests to /api/v1/posts
  4. Requires authentication for all other requests

Roles and Authorities

Spring Security distinguishes between roles and authorities:

  • Authorities are permissions like "READ_DATA" or "WRITE_DATA"
  • Roles are groups of permissions, conventionally prefixed with "ROLE_" (e.g., "ROLE_ADMIN")

When using the .hasRole("ADMIN") method, Spring Security automatically prepends "ROLE_" to "ADMIN".

Here's how to configure both roles and authorities:

java
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.authorities("READ_DATA")
.build();

UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN")
.authorities("READ_DATA", "WRITE_DATA")
.build();

return new InMemoryUserDetailsManager(user, admin);
}

And in your security configuration:

java
http.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/api/data/read").hasAuthority("READ_DATA")
.requestMatchers("/api/data/write").hasAuthority("WRITE_DATA")
.requestMatchers("/admin").hasRole("ADMIN")
);

Method Security

Spring Security allows you to secure individual methods in your services or controllers using annotations:

First, enable method security in your configuration:

java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig {
// Configuration here
}

Then use annotations to secure your methods:

java
@Service
public class UserService {

@PreAuthorize("hasRole('ADMIN')")
public List<User> getAllUsers() {
// Only accessible to admins
return userRepository.findAll();
}

@PreAuthorize("hasRole('ADMIN') or #username == authentication.principal.username")
public User getUser(String username) {
// Accessible to admins OR to the user themselves
return userRepository.findByUsername(username);
}

@PostAuthorize("returnObject.username == authentication.principal.username")
public User getUserById(Long id) {
// The method executes, but the result is only returned if condition is met
return userRepository.findById(id).orElse(null);
}
}

Common Security Expressions

Spring Security provides several expressions for use in @PreAuthorize, @PostAuthorize, and URL-based security:

ExpressionDescription
hasRole('ADMIN')User has role ROLE_ADMIN
hasAnyRole('ADMIN', 'USER')User has any of the given roles
hasAuthority('READ')User has the READ authority
hasAnyAuthority('READ', 'WRITE')User has any of the given authorities
isAnonymous()User is not authenticated
isAuthenticated()User is authenticated
isFullyAuthenticated()User is authenticated and not remembered
permitAll()Always evaluates to true
denyAll()Always evaluates to false
#username == authentication.principal.usernameUsername argument matches current user

Real-World Example: E-commerce Authorization

Let's apply authorization principles to a simplified e-commerce application:

java
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class EcommerceSecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authorize -> authorize
// Public pages
.requestMatchers("/", "/shop/**", "/product/**", "/search").permitAll()
.requestMatchers("/register", "/login", "/forgot-password").permitAll()
.requestMatchers("/css/**", "/js/**", "/images/**").permitAll()

// Customer areas
.requestMatchers("/cart/**", "/checkout/**").authenticated()
.requestMatchers("/account/**").authenticated()

// Merchant areas
.requestMatchers("/merchant/products/**").hasRole("MERCHANT")

// Admin areas
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/account")
.permitAll()
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.permitAll()
);

return http.build();
}

// Product service with method-level security
@Service
public class ProductService {

// Anyone can view active products
public List<Product> getActiveProducts() {
return productRepository.findByStatus("ACTIVE");
}

// Only merchants can see their draft products
@PreAuthorize("hasRole('MERCHANT')")
public List<Product> getMyDraftProducts(Long merchantId) {
return productRepository.findByMerchantIdAndStatus(merchantId, "DRAFT");
}

// Only the owner merchant or an admin can update a product
@PreAuthorize("hasRole('ADMIN') or " +
"(hasRole('MERCHANT') and @productSecurity.isMerchantOwner(#product.id, authentication.principal))")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
}
}

We've also created a custom security evaluator:

java
@Component
public class ProductSecurity {
private final ProductRepository productRepository;

public ProductSecurity(ProductRepository productRepository) {
this.productRepository = productRepository;
}

public boolean isMerchantOwner(Long productId, UserDetails userDetails) {
Product product = productRepository.findById(productId).orElse(null);
if (product == null) return false;

// Extract merchant ID from UserDetails implementation
MerchantUser merchantUser = (MerchantUser) userDetails;
return product.getMerchantId().equals(merchantUser.getMerchantId());
}
}

Advanced Authorization: Dynamic Permissions

Sometimes, authorization requirements are more dynamic. For instance, you might want to determine access based on data in the database.

Creating a Custom Permission Evaluator

java
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

private final ArticleRepository articleRepository;

public CustomPermissionEvaluator(ArticleRepository articleRepository) {
this.articleRepository = articleRepository;
}

@Override
public boolean hasPermission(Authentication auth, Object targetDomainObject, Object permission) {
if (auth == null || targetDomainObject == null || !(permission instanceof String)) {
return false;
}

// Check if user is editor of the article
if (targetDomainObject instanceof Article) {
Article article = (Article) targetDomainObject;
String username = auth.getName();

if ("edit".equals(permission)) {
return article.getAuthorUsername().equals(username) ||
auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_EDITOR"));
}
}

return false;
}

@Override
public boolean hasPermission(Authentication auth, Serializable targetId, String targetType, Object permission) {
if (auth == null || targetType == null || !(permission instanceof String)) {
return false;
}

// For article editing by ID
if ("article".equals(targetType)) {
String username = auth.getName();
Article article = articleRepository.findById((Long) targetId).orElse(null);

if (article != null && "edit".equals(permission)) {
return article.getAuthorUsername().equals(username) ||
auth.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_EDITOR"));
}
}

return false;
}
}

Configure it in your security configuration:

java
@Configuration
@EnableMethodSecurity
public class MethodSecurityConfig extends MethodSecurityExpressionHandler {

@Autowired
private CustomPermissionEvaluator permissionEvaluator;

@Bean
public MethodSecurityExpressionHandler createExpressionHandler() {
DefaultMethodSecurityExpressionHandler expressionHandler = new DefaultMethodSecurityExpressionHandler();
expressionHandler.setPermissionEvaluator(permissionEvaluator);
return expressionHandler;
}
}

Now you can use it in your service:

java
@Service
public class ArticleService {

@PreAuthorize("hasPermission(#articleId, 'article', 'edit')")
public Article updateArticle(Long articleId, ArticleUpdateDto updateData) {
// Update logic here
}

@PostAuthorize("hasPermission(returnObject, 'edit')")
public Article getArticleForEditing(Long articleId) {
// Get article logic here
}
}

Summary

Spring Security Authorization provides a robust and flexible system for controlling access to resources in your Spring applications. We've covered:

  1. Basic URL-based authorization patterns
  2. Roles vs. authorities and how to configure both
  3. Method-level security with @PreAuthorize, @PostAuthorize, etc.
  4. Security expressions for defining access rules
  5. Implementing real-world authorization scenarios
  6. Creating custom permission evaluators for dynamic authorization

With these tools, you can implement sophisticated access control in your applications, ensuring users can only access resources they're permitted to use.

Additional Resources

  1. Spring Security Official Documentation
  2. Spring Method Security Reference
  3. Expression-Based Access Control

Exercises

  1. Create a Spring Security configuration for a blog application where:

    • Anyone can view published posts
    • Authors can create and edit their own posts
    • Editors can edit any post
    • Admins can manage users and delete posts
  2. Implement method security for a service that allows users to view and edit their own profile information, but prevents them from editing others'.

  3. Create a custom permission evaluator that determines if a user can access a resource based on whether they belong to the same organization as the resource.



If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)