Skip to main content

Spring Security Roles

Introduction

Role-based access control (RBAC) is one of the fundamental concepts in application security. In a modern web application, different users often need different levels of access to various parts of the system. For example, administrators might need access to all features, while regular users may only access a subset of functionality.

Spring Security provides robust role management capabilities that allow you to:

  • Assign roles to users
  • Restrict access to endpoints based on roles
  • Check roles programmatically in your code
  • Create custom authorization rules

In this tutorial, we'll explore how to implement role-based security using Spring Security, with practical examples and best practices.

Understanding Roles in Spring Security

In Spring Security, roles represent a group of permissions. By default, Spring Security prefixes roles with "ROLE_", which is an important convention to remember. For example:

  • ROLE_ADMIN - An administrator role
  • ROLE_USER - A standard user role
  • ROLE_MANAGER - A manager role

While authorities and roles are technically similar in Spring Security, roles are a higher-level concept used specifically for grouping permissions. Behind the scenes, a role is just a special type of authority prefixed with "ROLE_".

Setting Up Spring Security for Role-based Authorization

Let's start by configuring Spring Security to use role-based authorization.

1. Add Spring Security Dependencies

First, you'll need to add Spring Security to your project:

xml
<!-- For Maven -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

Or for Gradle:

groovy
implementation 'org.springframework.boot:spring-boot-starter-security'

2. Configure Basic Security with Roles

Let's create a basic security configuration:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

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

return http.build();
}

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

UserDetails admin = User.withDefaultPasswordEncoder()
.username("admin")
.password("admin")
.roles("ADMIN", "USER") // Note: Admin also has USER role
.build();

return new InMemoryUserDetailsManager(user, admin);
}
}

In this example:

  • We're configuring two users: one with the "USER" role and another with both "ADMIN" and "USER" roles
  • URLs starting with /user/ require the "USER" role
  • URLs starting with /admin/ require the "ADMIN" role
  • The root path / is accessible to everyone
caution

User.withDefaultPasswordEncoder() is marked as deprecated and should only be used for demos or testing. For production environments, use a proper password encoder like BCryptPasswordEncoder.

Role-based Access Control for REST Endpoints

Using @PreAuthorize Annotation

For more fine-grained control, you can use method-level security with the @PreAuthorize annotation:

java
@Configuration
@EnableMethodSecurity // Enable method security
public class MethodSecurityConfig {
// Configuration goes here
}

Now you can secure your controller methods:

java
@RestController
@RequestMapping("/api")
public class UserController {

@GetMapping("/users")
@PreAuthorize("hasRole('USER')")
public List<User> getAllUsers() {
// This method is only accessible to users with ROLE_USER
return userService.findAll();
}

@GetMapping("/admins")
@PreAuthorize("hasRole('ADMIN')")
public String adminOnly() {
// This method is only accessible to users with ROLE_ADMIN
return "Hello Admin!";
}

@GetMapping("/managers")
@PreAuthorize("hasRole('MANAGER') and hasRole('USER')")
public String managersOnly() {
// This method requires both ROLE_MANAGER and ROLE_USER
return "Hello Manager!";
}
}

When a user without the required role tries to access a protected endpoint, they will receive a 403 Forbidden HTTP response.

Working with Multiple Roles

Often, you'll want to implement a role hierarchy or check for multiple roles. Let's explore how to do this.

Role Hierarchies

Spring Security lets you define role hierarchies, where higher roles automatically include the permissions of lower roles:

java
@Bean
public RoleHierarchy roleHierarchy() {
RoleHierarchyImpl hierarchy = new RoleHierarchyImpl();
hierarchy.setHierarchy(
"ROLE_ADMIN > ROLE_MANAGER\n" +
"ROLE_MANAGER > ROLE_USER"
);
return hierarchy;
}

@Bean
public SecurityExpressionHandler<FilterInvocation> expressionHandler(RoleHierarchy roleHierarchy) {
DefaultWebSecurityExpressionHandler webSecurityExpressionHandler = new DefaultWebSecurityExpressionHandler();
webSecurityExpressionHandler.setRoleHierarchy(roleHierarchy);
return webSecurityExpressionHandler;
}

With this configuration, users with ROLE_ADMIN automatically get the permissions of ROLE_MANAGER and ROLE_USER.

Complex Authorization Rules

For more complex rules, you can use Spring Expression Language (SpEL) in your @PreAuthorize annotations:

java
@GetMapping("/complex-auth")
@PreAuthorize("hasRole('ADMIN') or (hasRole('MANAGER') and #id == authentication.principal.id)")
public String complexAuth(@RequestParam Long id) {
// This method allows ADMIN access or MANAGER access if the ID matches their own
return "You passed the complex authorization check!";
}

Accessing Role Information in Your Code

Sometimes you need to access a user's roles in your business logic:

java
@Service
public class UserService {

public void doSomethingBasedOnRole() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication != null) {
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();

// Check if user has specific role
boolean isAdmin = authorities.stream()
.anyMatch(auth -> auth.getAuthority().equals("ROLE_ADMIN"));

if (isAdmin) {
// Do admin-specific logic
} else {
// Do regular user logic
}
}
}
}

Custom User Details with Roles

For real-world applications, you'll typically load users and their roles from a database:

java
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

private String username;
private String password;

@ElementCollection(fetch = FetchType.EAGER)
private Set<String> roles = new HashSet<>();

// getters and setters
}

@Repository
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}

@Service
public class CustomUserDetailsService implements UserDetailsService {

@Autowired
private UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));

return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.roles(user.getRoles().toArray(new String[0]))
.build();
}
}

Then, update your security configuration to use this custom service:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Autowired
private CustomUserDetailsService userDetailsService;

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

return http.build();
}

@Bean
public AuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setPasswordEncoder(passwordEncoder());
return provider;
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

Testing Role-Based Security

It's essential to test your security configurations. Here's an example of testing role-based security with Spring's testing support:

java
@SpringBootTest
@AutoConfigureMockMvc
public class SecurityTests {

@Autowired
private MockMvc mockMvc;

@Test
@WithMockUser(roles = "USER")
public void whenUserAccessUserEndpoint_thenOk() throws Exception {
mockMvc.perform(get("/user/info"))
.andExpect(status().isOk());
}

@Test
@WithMockUser(roles = "USER")
public void whenUserAccessAdminEndpoint_thenForbidden() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isForbidden());
}

@Test
@WithMockUser(roles = {"ADMIN", "USER"})
public void whenAdminAccessAdminEndpoint_thenOk() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().isOk());
}
}

Handling Access Denied

When a user tries to access a resource they don't have permission for, they receive a 403 Forbidden response. You can customize this behavior:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// existing configuration
.exceptionHandling(exception -> exception
.accessDeniedPage("/access-denied")
);

return http.build();
}

// Other beans
}

Then create a controller to handle the access denied page:

java
@Controller
public class ErrorController {

@GetMapping("/access-denied")
public String accessDenied() {
return "errors/access-denied"; // maps to a view template
}
}

Best Practices for Role-Based Security

  1. Follow the Principle of Least Privilege: Give users only the roles they absolutely need.

  2. Never Hard-Code Roles: Use constants or enums for role names to avoid typos.

java
public final class SecurityRoles {
public static final String ROLE_ADMIN = "ROLE_ADMIN";
public static final String ROLE_USER = "ROLE_USER";
public static final String ROLE_MANAGER = "ROLE_MANAGER";

private SecurityRoles() {} // Prevent instantiation
}
  1. Implement Role Hierarchies: For complex systems, use hierarchies to simplify permissions.

  2. Secure All Access Points: Don't forget to secure APIs, WebSockets, and other entry points.

  3. Combine with Method Security: Use both global and method-level security for defense in depth.

  4. Log Security Events: Monitor failed access attempts and role changes.

Summary

Spring Security's role-based access control provides a powerful and flexible way to implement authorization in your applications. In this tutorial, we've covered:

  • Setting up basic role-based security
  • Configuring method-level security with annotations
  • Working with multiple roles and hierarchies
  • Accessing role information in your code
  • Implementing custom user details with role information
  • Testing role-based security
  • Handling access denied scenarios
  • Best practices for role-based security

By properly implementing roles, you can ensure that users only have access to the functionality they need, enhancing the security of your Spring applications.

Additional Resources

Exercises

  1. Create a Spring Security configuration with three roles: USER, MANAGER, and ADMIN with appropriate role hierarchies.

  2. Implement a REST API with different endpoints secured by different role requirements.

  3. Extend the application to load user roles from a database instead of in-memory authentication.

  4. Implement a custom access denied handler that returns a JSON response for REST APIs.

  5. Create a system that allows administrators to assign roles to users dynamically.



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